From bb2097b50d0aaec9e28f81d0846f7fef93798db0 Mon Sep 17 00:00:00 2001
From: Melvyn Depeyrot <melvyn.depeyrot@gmail.com>
Date: Wed, 30 Sep 2020 15:09:30 -0700
Subject: [PATCH 01/19] WebAuth: Add oauth login functionality (#211)

---
 steam/webauth.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 64 insertions(+), 1 deletion(-)

diff --git a/steam/webauth.py b/steam/webauth.py
index d81ce13d..1b2c8fc0 100644
--- a/steam/webauth.py
+++ b/steam/webauth.py
@@ -61,7 +61,6 @@
 import six
 import requests
 
-from steam import webapi
 from steam.steamid import SteamID
 from steam.utils.web import make_requests_session, generate_session_id
 from steam.core.crypto import rsa_publickey, pkcs1v15_encrypt
@@ -323,6 +322,70 @@ def _finalize_login(self, login_response):
         self.steam_id = SteamID(data['steamid'])
         self.oauth_token = data['oauth_token']
 
+    def oauth_login(self, oauth_token='', steam_id='', language='english'):
+        """Attempts a mobile authenticator login using an oauth token, which can be obtained from a previously logged-in
+        `MobileWebAuth`
+
+        :param oauth_token: oauth token string, if it wasn't provided on instance init
+        :type  oauth_token: :class:`str`
+        :param steam_id: `SteamID` of the account to log into, if it wasn't provided on instance init
+        :type  steam_id: :class:`str` or :class:`SteamID`
+        :param language: select language for steam web pages (sets language cookie)
+        :type  language: :class:`str`
+        :return: a session on success and :class:`None` otherwise
+        :rtype: :class:`requests.Session`, :class:`None`
+        :raises HTTPError: any problem with http request, timeouts, 5xx, 4xx etc
+        :raises LoginIncorrect: Invalid token or SteamID
+        """
+        if oauth_token:
+            self.oauth_token = oauth_token
+        else:
+            if self.oauth_token:
+                oauth_token = self.oauth_token
+            else:
+                raise LoginIncorrect('token is not specified')
+
+        if steam_id:
+            self.steam_id = SteamID(steam_id)
+        else:
+            if not self.steam_id:
+                raise LoginIncorrect('steam_id is not specified')
+
+        steam_id = self.steam_id.as_64
+
+        data = {
+            'access_token': oauth_token
+        }
+
+        try:
+            resp = self.session.post('https://api.steampowered.com/IMobileAuthService/GetWGToken/v0001', data=data)
+        except requests.exceptions.RequestException as e:
+            raise HTTPError(str(e))
+
+        try:
+            resp_data = resp.json()['response']
+        except json.decoder.JSONDecodeError as e:
+            if 'Please verify your <pre>key=</pre> parameter.' in resp.text:
+                raise LoginIncorrect('invalid token')
+            else:
+                raise e
+
+        self.session_id = generate_session_id()
+
+        for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']:
+            self.session.cookies.set('birthtime', '-3333', domain=domain)
+            self.session.cookies.set('sessionid', self.session_id, domain=domain)
+            self.session.cookies.set('mobileClientVersion', '0 (2.1.3)', domain=domain)
+            self.session.cookies.set('mobileClient', 'android', domain=domain)
+            self.session.cookies.set('steamLogin', str(steam_id) + "%7C%7C" + resp_data['token'], domain=domain)
+            self.session.cookies.set('steamLoginSecure', str(steam_id) + "%7C%7C" + resp_data['token_secure'],
+                                     domain=domain, secure=True)
+            self.session.cookies.set('Steam_Language', language, domain=domain)
+
+        self.logged_on = True
+
+        return self.session
+
 
 class WebAuthException(Exception):
     pass

From 5524215944ff1b53b99bf9017c84ba96f3560829 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Tue, 15 Dec 2020 20:54:24 +0000
Subject: [PATCH 02/19] get_product_info fills access_tokens automatically

---
 steam/client/builtins/apps.py | 29 ++++++++++++++++++++++++-----
 1 file changed, 24 insertions(+), 5 deletions(-)

diff --git a/steam/client/builtins/apps.py b/steam/client/builtins/apps.py
index 64dedb05..ee7fe3ed 100644
--- a/steam/client/builtins/apps.py
+++ b/steam/client/builtins/apps.py
@@ -41,13 +41,15 @@ def get_player_count(self, app_id, timeout=5):
         else:
             return EResult(resp.eresult)
 
-    def get_product_info(self, apps=[], packages=[], timeout=15):
+    def get_product_info(self, apps=[], packages=[], auto_access_tokens=True, timeout=15):
         """Get product info for apps and packages
 
         :param apps: items in the list should be either just ``app_id``, or :class:`dict`
-        :type apps: :class:`list`
+        :type  apps: :class:`list`
         :param packages: items in the list should be either just ``package_id``, or :class:`dict`
-        :type packages: :class:`list`
+        :type  packages: :class:`list`
+        :param auto_access_token: automatically request and fill access tokens
+        :type  auto_access_token: :class:`bool`
         :return: dict with ``apps`` and ``packages`` containing their info, see example below
         :rtype: :class:`dict`, :class:`None`
 
@@ -90,11 +92,24 @@ def get_product_info(self, apps=[], packages=[], timeout=15):
         if not apps and not packages:
             return
 
+        if auto_access_tokens:
+            tokens = self.get_access_tokens(app_ids=list(map(lambda app: app['appid'] if isinstance(app, dict) else app, apps)),
+                                            package_ids=list(map(lambda pkg: pkg['packageid'] if isinstance(pkg, dict) else pkg, packages))
+                                            )
+        else:
+            tokens = None
+
+        print(tokens)
+
         message = MsgProto(EMsg.ClientPICSProductInfoRequest)
 
         for app in apps:
                 app_info = message.body.apps.add()
                 app_info.only_public = False
+
+                if tokens:
+                    app_info.access_token = tokens['apps'].get(app['appid'] if isinstance(app, dict) else app, 0)
+
                 if isinstance(app, dict):
                         proto_fill_from_dict(app_info, app)
                 else:
@@ -102,6 +117,10 @@ def get_product_info(self, apps=[], packages=[], timeout=15):
 
         for package in packages:
                 package_info = message.body.packages.add()
+
+                if tokens:
+                    package_info.access_token = tokens['packages'].get(package['packageid'] if isinstance(package, dict) else package, 0)
+
                 if isinstance(package, dict):
                         proto_fill_from_dict(package_info, package)
                 else:
@@ -163,7 +182,7 @@ def get_app_ticket(self, app_id):
                                       {'app_id': app_id},
                                       timeout=10
                                       )
-    
+
     def get_encrypted_app_ticket(self, app_id, userdata):
         """Gets the encrypted app ticket
         :param app_id: app id
@@ -246,7 +265,7 @@ def get_access_tokens(self, app_ids=[], package_ids=[]):
 
         if resp:
             return {'apps': dict(map(lambda app: (app.appid, app.access_token), resp.app_access_tokens)),
-                    'packages': dict(map(lambda pkg: (pkg.appid, pkg.access_token), resp.package_access_tokens)),
+                    'packages': dict(map(lambda pkg: (pkg.packageid, pkg.access_token), resp.package_access_tokens)),
                     }
 
     def register_product_key(self, key):

From 58bd4a299a24466ba7b118feda653ab98dc6cf29 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Tue, 15 Dec 2020 20:57:09 +0000
Subject: [PATCH 03/19] bump to v1.1.0

---
 steam/__init__.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/steam/__init__.py b/steam/__init__.py
index 9ff48997..d5112d85 100644
--- a/steam/__init__.py
+++ b/steam/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "1.0.2"
+__version__ = "1.1.0"
 __author__ = "Rossen Georgiev"
 
-version_info = (1, 0, 2)
+version_info = (1, 1, 0)

From 4531d4adda9bfcee8796a911c14526971bc99b1b Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Tue, 15 Dec 2020 21:03:13 +0000
Subject: [PATCH 04/19] guard: syntax error on one of the exceptions

---
 steam/guard.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/steam/guard.py b/steam/guard.py
index a15bccc8..4cddc55a 100644
--- a/steam/guard.py
+++ b/steam/guard.py
@@ -148,8 +148,8 @@ def _send_request(self, action, params):
             if resp is None:
                 raise SteamAuthenticatorError("Failed. Request timeout")
             if resp.header.eresult != EResult.OK:
-                raise SteamAuthenticatorError("Failed: %s (%s)" % str(resp.header.error_message,
-                                                                      repr(resp.header.eresult)))
+                raise SteamAuthenticatorError("Failed: %s (%s)" % (resp.header.error_message,
+                                                                   repr(resp.header.eresult)))
 
             resp = proto_to_dict(resp.body)
 

From 3888aa60432ad201890a458031baed9043df0b6e Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Tue, 15 Dec 2020 21:12:04 +0000
Subject: [PATCH 05/19] bump to v1.1.1

---
 steam/__init__.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/steam/__init__.py b/steam/__init__.py
index d5112d85..3302c8b0 100644
--- a/steam/__init__.py
+++ b/steam/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "1.1.0"
+__version__ = "1.1.1"
 __author__ = "Rossen Georgiev"
 
-version_info = (1, 1, 0)
+version_info = (1, 1, 1)

From 6826eee5df0612945c83bf96774df5f47b623580 Mon Sep 17 00:00:00 2001
From: Rossen <2720787+rossengeorgiev@users.noreply.github.com>
Date: Wed, 16 Dec 2020 20:17:25 +0000
Subject: [PATCH 06/19] Migrate from travis-ci to Github Actions (#300)

---
 .github/workflows/testing_initiative.yml | 81 ++++++++++++++++++++++++
 .travis.yml                              | 65 -------------------
 2 files changed, 81 insertions(+), 65 deletions(-)
 create mode 100644 .github/workflows/testing_initiative.yml
 delete mode 100644 .travis.yml

diff --git a/.github/workflows/testing_initiative.yml b/.github/workflows/testing_initiative.yml
new file mode 100644
index 00000000..f660d645
--- /dev/null
+++ b/.github/workflows/testing_initiative.yml
@@ -0,0 +1,81 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Testing Initiative
+
+on:
+  push:
+    branches: [ master ]
+    paths-ignore:
+      - '.gitignore'
+      - '*.md'
+      - '*.rst'
+      - 'LICENSE'
+      - 'Vagrantfile'
+      - 'protobuf_list.txt'
+      - 'protobufs/**'
+      - 'recipes/**'
+  pull_request:
+    branches: [ master ]
+    paths-ignore:
+      - '.gitignore'
+      - '*.md'
+      - '*.rst'
+      - 'LICENSE'
+      - 'Vagrantfile'
+      - 'protobuf_list.txt'
+      - 'protobufs/**'
+      - 'recipes/**'
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
+#       exclude:
+#         - os: macos-latest
+#           python-version: 3.8
+#         - os: windows-latest
+#           python-version: 3.6
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python Env
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Display Python version
+        run: python -c "import sys; print(sys.version)"
+      - name: Install dependencies
+        run: |
+          pip install -r requirements.txt
+          pip install coveralls
+      - name: Run Tests
+        env:
+          PYTHONHASHSEED: "0"
+        run: |
+          pytest --cov=steam tests
+      - name: Coveralls
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+        run: |
+          coveralls
+  build-docs:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        python-version: [3.6]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python Env
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Display Python version
+        run: python -c "import sys; print(sys.version)"
+      - name: Install dependencies
+        run: make init
+      - name: Build Docs
+        run: make docs
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index c89dfed3..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-language: python
-os: linux
-jobs:
-  include:
-# docs build
-    - name: Docs (py36)
-      python: 3.6
-      install: make init
-      script: make docs
-      after_script: []
-# linux
-    - python: 2.7
-    - python: 3.5
-    - python: 3.6
-    - python: 3.7
-    - python: 3.8
-# OSX
-    - name: OSX Python 2.7
-      os: osx
-      language: shell
-      before_install:
-        - cp -fv `which python2` `which python` || true
-        - cp -fv `which pip2` `which pip` || true
-        - pip install --upgrade pip
-      after_script: []
-    - name: OSX Python 3.7
-      os: osx
-      language: shell
-      before_install:
-        - cp -fv `which python3` `which python` || true
-        - cp -fv `which pip3` `which pip` || true
-        - pip install --upgrade pip
-      after_script: []
-# Windows
-    - name: Win Python 3.6
-      language: shell
-      os: windows
-      env: PATH=/c/Python36:/c/Python36/Scripts:$PATH
-      before_install:
-        - choco install python --version 3.6.8
-        - python -m pip install --upgrade pip
-      after_script: []
-    - name: Win Python 3.7
-      language: shell
-      os: windows
-      env: PATH=/c/Python37:/c/Python37/Scripts:$PATH
-      before_install:
-        - choco install python --version 3.7.4
-        - python -m pip install --upgrade pip
-      after_script: []
-    - name: Win Python 3.8
-      language: shell
-      os: windows
-      env: PATH=/c/Python38:/c/Python38/Scripts:$PATH
-      before_install:
-        - choco install python --version 3.8.2
-        - python -m pip install --upgrade pip
-      after_script: []
-install:
-    - pip install -r requirements.txt
-    - pip install coveralls
-script:
-    - PYTHONHASHSEED=0 pytest --cov=steam tests
-after_script:
-    - coveralls

From 1430403d5f0034bc22e199bad4ffec54232b5f33 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Tue, 19 Jan 2021 21:39:49 +0000
Subject: [PATCH 07/19] refactor requirements and fix coveralls

---
 .coveragerc                              |  9 +++++++
 .github/workflows/testing_initiative.yml | 30 +++++++++++++++++-------
 Makefile                                 |  4 ++--
 README.rst                               |  4 ++--
 dev_requirements.txt                     | 12 ++++++++++
 requirements.txt                         |  5 ----
 6 files changed, 47 insertions(+), 17 deletions(-)
 create mode 100644 dev_requirements.txt

diff --git a/.coveragerc b/.coveragerc
index 0e530720..6c8265c5 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,5 +1,14 @@
+
+# Docs:  https://coverage.readthedocs.org/en/latest/config.html
+
 [run]
 concurrency = gevent
 omit =
     steam/protobufs/*
     steam/enums/*
+
+branch = False
+
+# If True, stores relative file paths in data file (needed for Github Actions).
+# Using this parameter requires coverage>=5.0
+relative_files = True
diff --git a/.github/workflows/testing_initiative.yml b/.github/workflows/testing_initiative.yml
index f660d645..635607e3 100644
--- a/.github/workflows/testing_initiative.yml
+++ b/.github/workflows/testing_initiative.yml
@@ -48,19 +48,33 @@ jobs:
         run: python -c "import sys; print(sys.version)"
       - name: Install dependencies
         run: |
-          pip install -r requirements.txt
-          pip install coveralls
+          make init
       - name: Run Tests
-        env:
-          PYTHONHASHSEED: "0"
         run: |
-          pytest --cov=steam tests
-      - name: Coveralls
+          make test
+      - name: Upload to Coveralls
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+          COVERALLS_PARALLEL: true
+          COVERALLS_FLAG_NAME: "${{ matrix.os }}_${{ matrix.python-version }}"
         run: |
-          coveralls
+          coveralls --service=github
+
+  coveralls:
+    name: Finish Coveralls
+    needs: test
+    runs-on: ubuntu-latest
+    container: python:3-slim
+    steps:
+    - name: Install coveralls
+      run: |
+        pip3 install --upgrade coveralls
+    - name: Send coverage finish to coveralls.io
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: |
+        coveralls --finish
+
   build-docs:
     runs-on: ${{ matrix.os }}
     strategy:
diff --git a/Makefile b/Makefile
index 5ab7ae7f..4a2618da 100644
--- a/Makefile
+++ b/Makefile
@@ -25,14 +25,14 @@ help:
 	@echo "$$HELPBODY"
 
 init: init_docs
-	pip install -r requirements.txt
+	pip install -r dev_requirements.txt
 
 init_docs:
 	pip install sphinx==1.8.5 sphinx_rtd_theme
 
 test:
 	coverage erase
-	PYTHONHASHSEED=0 pytest --cov=steam tests
+	PYTHONHASHSEED=0 pytest --tb=short --cov-config .coveragerc --cov=steam tests
 
 webauth_gen:
 	rm -f vcr/webauth*
diff --git a/README.rst b/README.rst
index 755979df..b944b0ed 100644
--- a/README.rst
+++ b/README.rst
@@ -113,8 +113,8 @@ IRC: irc.freenode.net / #steamkit (`join via webchat <https://webchat.freenode.n
     :target: https://pypi.python.org/pypi/steam
     :alt: MIT License
 
-.. |coverage| image:: https://scrutinizer-ci.com/g/ValvePython/steam/badges/coverage.png?b=master
-    :target: https://scrutinizer-ci.com/g/ValvePython/steam/?branch=master
+.. |coverage| image:: https://img.shields.io/coveralls/ValvePython/steam/master.svg?style=flat
+    :target: https://coveralls.io/r/ValvePython/steam?branch=master
     :alt: Test coverage
 
 .. |sonar_maintainability| image:: https://sonarcloud.io/api/project_badges/measure?project=ValvePython_steam&metric=sqale_rating
diff --git a/dev_requirements.txt b/dev_requirements.txt
new file mode 100644
index 00000000..dad8afb6
--- /dev/null
+++ b/dev_requirements.txt
@@ -0,0 +1,12 @@
+
+-r requirements.txt
+
+vcrpy==2.0.1
+mock==1.3.0
+
+coverage>=5.0; python_version == '2.7' or python_version >= '3.5'
+pytest-cov>=2.7.0; python_version == '2.7' or python_version >= '3.5'
+
+# coveralls 2.0 has removed support for Python 2.7 and 3.4
+git+https://github.com/andy-maier/coveralls-python.git@andy/add-py27#egg=coveralls; python_version == '2.7'
+coveralls>=2.1.2; python_version >= '3.5'
diff --git a/requirements.txt b/requirements.txt
index dadda5d4..af1ffd9b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,10 +6,5 @@ gevent>=1.3.0
 protobuf>=3.0.0
 gevent-eventemitter>=2.1
 enum34==1.1.2; python_version < '3.4'
-coverage==4.0.3
-pytest==3.2.1
-pytest-cov==2.5.1
-mock==1.3.0
 PyYAML==5.1
-vcrpy==2.0.1
 cachetools>=3.0.0

From 566735b5330fd7d9d37d021ef66e6dd1e485048b Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Wed, 20 Jan 2021 20:56:59 +0000
Subject: [PATCH 08/19] remove stray print statement in get_product_info

---
 steam/client/builtins/apps.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/steam/client/builtins/apps.py b/steam/client/builtins/apps.py
index ee7fe3ed..c00ff836 100644
--- a/steam/client/builtins/apps.py
+++ b/steam/client/builtins/apps.py
@@ -99,8 +99,6 @@ def get_product_info(self, apps=[], packages=[], auto_access_tokens=True, timeou
         else:
             tokens = None
 
-        print(tokens)
-
         message = MsgProto(EMsg.ClientPICSProductInfoRequest)
 
         for app in apps:

From c50d84be3d9820d26a706ec6991edd0ec8e4c21e Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Wed, 20 Jan 2021 22:42:16 +0000
Subject: [PATCH 09/19] get_product_info can now request only meta data

---
 steam/client/builtins/apps.py | 27 +++++++++++++++++++++++----
 1 file changed, 23 insertions(+), 4 deletions(-)

diff --git a/steam/client/builtins/apps.py b/steam/client/builtins/apps.py
index c00ff836..ecbbe805 100644
--- a/steam/client/builtins/apps.py
+++ b/steam/client/builtins/apps.py
@@ -41,13 +41,15 @@ def get_player_count(self, app_id, timeout=5):
         else:
             return EResult(resp.eresult)
 
-    def get_product_info(self, apps=[], packages=[], auto_access_tokens=True, timeout=15):
+    def get_product_info(self, apps=[], packages=[], meta_data_only=False, auto_access_tokens=True, timeout=15):
         """Get product info for apps and packages
 
         :param apps: items in the list should be either just ``app_id``, or :class:`dict`
         :type  apps: :class:`list`
         :param packages: items in the list should be either just ``package_id``, or :class:`dict`
         :type  packages: :class:`list`
+        :param meta_data_only: only meta data will be returned in the reponse (e.g. change number, missing_token, sha1)
+        :type  meta_data_only: :class:`bool`
         :param auto_access_token: automatically request and fill access tokens
         :type  auto_access_token: :class:`bool`
         :return: dict with ``apps`` and ``packages`` containing their info, see example below
@@ -124,7 +126,9 @@ def get_product_info(self, apps=[], packages=[], auto_access_tokens=True, timeou
                 else:
                         package_info.packageid = package
 
-        message.body.meta_data_only = False
+        message.body.meta_data_only = meta_data_only
+        message.body.num_prev_failed = 0
+        message.body.supports_package_tokens = 1
 
         job_id = self.send_job(message)
 
@@ -136,11 +140,26 @@ def get_product_info(self, apps=[], packages=[], auto_access_tokens=True, timeou
             chunk = chunk[0].body
 
             for app in chunk.apps:
-                data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode('utf-8', 'replace'))['appinfo']
+                if app.buffer:
+                    data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode('utf-8', 'replace'))['appinfo']
+                else:
+                    data['apps'][app.appid] = {}
+
                 data['apps'][app.appid]['_missing_token'] = app.missing_token
+                data['apps'][app.appid]['_change_number'] = app.change_number
+                data['apps'][app.appid]['_sha'] = app.sha
+                data['apps'][app.appid]['_size'] = app.size
+
             for pkg in chunk.packages:
-                data['packages'][pkg.packageid] = vdf.binary_loads(pkg.buffer[4:]).get(str(pkg.packageid), {})
+                if pkg.buffer:
+                    data['packages'][pkg.packageid] = vdf.binary_loads(pkg.buffer[4:]).get(str(pkg.packageid), {})
+                else:
+                    data['packages'][pkg.packageid] = {}
+
                 data['packages'][pkg.packageid]['_missing_token'] = pkg.missing_token
+                data['packages'][pkg.packageid]['_change_number'] = pkg.change_number
+                data['packages'][pkg.packageid]['_sha'] = pkg.sha
+                data['packages'][pkg.packageid]['_size'] = pkg.size
 
             if not chunk.response_pending:
                 break

From 38663328731bc5fcf71848f8ad2a2bb6b5641be5 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Thu, 21 Jan 2021 00:26:26 +0000
Subject: [PATCH 10/19] add license and billing enums

---
 steam/enums/common.py | 47 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/steam/enums/common.py b/steam/enums/common.py
index 6cd29302..3d86ee8c 100644
--- a/steam/enums/common.py
+++ b/steam/enums/common.py
@@ -896,6 +896,53 @@ class EPurchaseResultDetail(SteamIntEnum):
     PaymentMethodNotSupportedForProduct = 83
 
 
+class ELicenseFlags(SteamIntEnum):
+    NONE = 0
+    Renew = 0x01
+    RenewalFailed = 0x02
+    Pending = 0x04
+    Expired = 0x08
+    CancelledByUser = 0x10
+    CancelledByAdmin = 0x20
+    LowViolenceContent = 0x40
+    ImportedFromSteam2 = 0x80
+    ForceRunRestriction = 0x100
+    RegionRestrictionExpired = 0x200
+    CancelledByFriendlyFraudLock = 0x400
+    NotActivated = 0x800
+
+
+class ELicenseType(SteamIntEnum):
+    NoLicense = 0
+    SinglePurchase = 1
+    SinglePurchaseLimitedUse = 2
+    RecurringCharge = 3
+    RecurringChargeLimitedUse = 4
+    RecurringChargeLimitedUseWithOverages = 5
+    RecurringOption = 6
+    LimitedUseDelayedActivation = 7
+
+
+class EBillingType(SteamIntEnum):
+    NoCost = 0
+    BillOnceOnly = 1
+    BillMonthly = 2
+    ProofOfPrepurchaseOnly = 3
+    GuestPass = 4
+    HardwarePromo = 5
+    Gift = 6
+    AutoGrant = 7
+    OEMTicket = 8
+    RecurringOption = 9
+    BillOnceOrCDKey = 10
+    Repurchaseable = 11
+    FreeOnDemand = 12
+    Rental = 13
+    CommercialLicense = 14
+    FreeCommercialLicense = 15
+    NumBillingTypes = 16
+
+
 # Do not remove
 from enum import EnumMeta
 

From 0ca3432acf1ec23b1e8cfc4dbc072ab6ef7ddb3b Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Wed, 3 Feb 2021 23:43:10 +0000
Subject: [PATCH 11/19] limit requirement for gevent-eventemitter

rep for https://github.com/rossengeorgiev/gevent-eventemitter/issues/4
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index af1ffd9b..d5e0306d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@ requests>=2.9.1
 vdf>=3.3
 gevent>=1.3.0
 protobuf>=3.0.0
-gevent-eventemitter>=2.1
+gevent-eventemitter~=2.1
 enum34==1.1.2; python_version < '3.4'
 PyYAML==5.1
 cachetools>=3.0.0

From 71309e527a8982be6df1678204dc3c71de10710e Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Thu, 4 Feb 2021 00:10:59 +0000
Subject: [PATCH 12/19] add raw parm to get_product_info()

Allows for the raw buffer to be returned in the response
---
 steam/client/builtins/apps.py | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/steam/client/builtins/apps.py b/steam/client/builtins/apps.py
index ecbbe805..8790630f 100644
--- a/steam/client/builtins/apps.py
+++ b/steam/client/builtins/apps.py
@@ -41,7 +41,7 @@ def get_player_count(self, app_id, timeout=5):
         else:
             return EResult(resp.eresult)
 
-    def get_product_info(self, apps=[], packages=[], meta_data_only=False, auto_access_tokens=True, timeout=15):
+    def get_product_info(self, apps=[], packages=[], meta_data_only=False, raw=False, auto_access_tokens=True, timeout=15):
         """Get product info for apps and packages
 
         :param apps: items in the list should be either just ``app_id``, or :class:`dict`
@@ -50,6 +50,8 @@ def get_product_info(self, apps=[], packages=[], meta_data_only=False, auto_acce
         :type  packages: :class:`list`
         :param meta_data_only: only meta data will be returned in the reponse (e.g. change number, missing_token, sha1)
         :type  meta_data_only: :class:`bool`
+        :param raw: Data buffer for each app is returned as bytes in its' original form. Apps buffer is text VDF, and package buffer is binary VDF
+        :type  raw: :class:`bool`
         :param auto_access_token: automatically request and fill access tokens
         :type  auto_access_token: :class:`bool`
         :return: dict with ``apps`` and ``packages`` containing their info, see example below
@@ -140,7 +142,7 @@ def get_product_info(self, apps=[], packages=[], meta_data_only=False, auto_acce
             chunk = chunk[0].body
 
             for app in chunk.apps:
-                if app.buffer:
+                if app.buffer and not raw:
                     data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode('utf-8', 'replace'))['appinfo']
                 else:
                     data['apps'][app.appid] = {}
@@ -150,8 +152,11 @@ def get_product_info(self, apps=[], packages=[], meta_data_only=False, auto_acce
                 data['apps'][app.appid]['_sha'] = app.sha
                 data['apps'][app.appid]['_size'] = app.size
 
+                if app.buffer and raw:
+                    data['apps'][app.appid]['_buffer'] = app.buffer
+
             for pkg in chunk.packages:
-                if pkg.buffer:
+                if pkg.buffer and not raw:
                     data['packages'][pkg.packageid] = vdf.binary_loads(pkg.buffer[4:]).get(str(pkg.packageid), {})
                 else:
                     data['packages'][pkg.packageid] = {}
@@ -161,6 +166,9 @@ def get_product_info(self, apps=[], packages=[], meta_data_only=False, auto_acce
                 data['packages'][pkg.packageid]['_sha'] = pkg.sha
                 data['packages'][pkg.packageid]['_size'] = pkg.size
 
+                if pkg.buffer and raw:
+                    data['packages'][pkg.packageid]['_buffer'] = pkg.buffer
+
             if not chunk.response_pending:
                 break
 

From ae4a37946af0c623eba317753b41c7bc966abfd4 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Fri, 5 Feb 2021 23:46:25 +0000
Subject: [PATCH 13/19] get_product_info: _sha in hex representation

This garantees that the output is serializable by JSON (no bytes objects)
---
 steam/client/builtins/apps.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/steam/client/builtins/apps.py b/steam/client/builtins/apps.py
index 8790630f..68812abb 100644
--- a/steam/client/builtins/apps.py
+++ b/steam/client/builtins/apps.py
@@ -1,3 +1,4 @@
+from binascii import hexlify
 import vdf
 from steam.enums import EResult, EServerType
 from steam.enums.emsg import EMsg
@@ -149,7 +150,7 @@ def get_product_info(self, apps=[], packages=[], meta_data_only=False, raw=False
 
                 data['apps'][app.appid]['_missing_token'] = app.missing_token
                 data['apps'][app.appid]['_change_number'] = app.change_number
-                data['apps'][app.appid]['_sha'] = app.sha
+                data['apps'][app.appid]['_sha'] = hexlify(app.sha).decode('ascii')
                 data['apps'][app.appid]['_size'] = app.size
 
                 if app.buffer and raw:
@@ -163,7 +164,7 @@ def get_product_info(self, apps=[], packages=[], meta_data_only=False, raw=False
 
                 data['packages'][pkg.packageid]['_missing_token'] = pkg.missing_token
                 data['packages'][pkg.packageid]['_change_number'] = pkg.change_number
-                data['packages'][pkg.packageid]['_sha'] = pkg.sha
+                data['packages'][pkg.packageid]['_sha'] = hexlify(pkg.sha).decode('ascii')
                 data['packages'][pkg.packageid]['_size'] = pkg.size
 
                 if pkg.buffer and raw:

From 30cc521b0465a43cbfcbe019291714b558a4f318 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Fri, 5 Feb 2021 23:47:55 +0000
Subject: [PATCH 14/19] bump v1.2.0

---
 steam/__init__.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/steam/__init__.py b/steam/__init__.py
index 3302c8b0..e737a667 100644
--- a/steam/__init__.py
+++ b/steam/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "1.1.1"
+__version__ = "1.2.0"
 __author__ = "Rossen Georgiev"
 
-version_info = (1, 1, 1)
+version_info = (1, 2, 0)

From 9ddb1ba68f082d3d9e87248173ecd114bbcdb2f1 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Fri, 5 Feb 2021 23:57:54 +0000
Subject: [PATCH 15/19] README: update relase badges

---
 README.rst | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.rst b/README.rst
index b944b0ed..7ffb9f8e 100644
--- a/README.rst
+++ b/README.rst
@@ -98,13 +98,13 @@ IRC: irc.freenode.net / #steamkit (`join via webchat <https://webchat.freenode.n
 
 .. _Steam: https://store.steampowered.com/
 
-.. |pypi| image:: https://img.shields.io/github/tag/valvepython/steam.svg?label=release&color=green&logo=steam
+.. |pypi| image:: https://img.shields.io/pypi/v/steam.svg?label=pypi&color=green
     :target: https://pypi.python.org/pypi/steam
     :alt: Latest version released on PyPi
 
-.. |latest| image:: https://img.shields.io/github/tag-pre/valvepython/steam.svg?label=latest&logo=steam
+.. |latest| image:: https://img.shields.io/github/v/tag/ValvePython/steam?include_prereleases&sort=semver&label=release
    :target: https://github.com/ValvePython/steam/releases
-   :alt: GitHub Releases
+   :alt: Latest release on Github
 
 .. |pypipy| image:: https://img.shields.io/pypi/pyversions/steam.svg?label=%20&logo=python&logoColor=white
     :alt: PyPI - Python Version

From fbea856715926c980ff96b90cddb7a9be134e0a8 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Sat, 6 Feb 2021 00:12:41 +0000
Subject: [PATCH 16/19] ci: include py3.9 + pypi no cov

---
 .github/workflows/testing_initiative.yml | 23 ++++++++++++++++-------
 Makefile                                 | 10 ++++++++--
 setup.py                                 |  1 +
 3 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/testing_initiative.yml b/.github/workflows/testing_initiative.yml
index 635607e3..15576f86 100644
--- a/.github/workflows/testing_initiative.yml
+++ b/.github/workflows/testing_initiative.yml
@@ -32,12 +32,15 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
-#       exclude:
-#         - os: macos-latest
-#           python-version: 3.8
-#         - os: windows-latest
-#           python-version: 3.6
+        python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
+        no-coverage: [0]
+        include:
+          - os: ubuntu-latest
+            python-version: pypy-2.7
+            no-coverage: 1
+          - os: ubuntu-latest
+            python-version: pypy-3.6
+            no-coverage: 1
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python Env
@@ -50,9 +53,13 @@ jobs:
         run: |
           make init
       - name: Run Tests
+        env:
+            NOCOV: ${{ matrix.no-coverage }}
         run: |
           make test
       - name: Upload to Coveralls
+        # pypy + concurrenct=gevent not supported in coveragepy. See https://github.com/nedbat/coveragepy/issues/560
+        if: matrix.no-coverage == 0
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           COVERALLS_PARALLEL: true
@@ -90,6 +97,8 @@ jobs:
       - name: Display Python version
         run: python -c "import sys; print(sys.version)"
       - name: Install dependencies
-        run: make init
+        run: |
+          make init
+          make init_docs
       - name: Build Docs
         run: make docs
diff --git a/Makefile b/Makefile
index 4a2618da..86c4f63e 100644
--- a/Makefile
+++ b/Makefile
@@ -24,15 +24,21 @@ export HELPBODY
 help:
 	@echo "$$HELPBODY"
 
-init: init_docs
+init:
 	pip install -r dev_requirements.txt
 
 init_docs:
 	pip install sphinx==1.8.5 sphinx_rtd_theme
 
+COVOPTS = --cov-config .coveragerc --cov=steam
+
+ifeq ($(NOCOV), 1)
+	COVOPTS =
+endif
+
 test:
 	coverage erase
-	PYTHONHASHSEED=0 pytest --tb=short --cov-config .coveragerc --cov=steam tests
+	PYTHONHASHSEED=0 pytest --tb=short $(COVOPTS) tests
 
 webauth_gen:
 	rm -f vcr/webauth*
diff --git a/setup.py b/setup.py
index aa93c38a..1c093d53 100644
--- a/setup.py
+++ b/setup.py
@@ -53,6 +53,7 @@
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: Implementation :: PyPy',
     ],
     keywords='valve steam steamid api webapi steamcommunity',

From f33cb3276c5e1dd2f341c2ff85e4230e61e9a2d5 Mon Sep 17 00:00:00 2001
From: Rossen Georgiev <rossen@rgp.io>
Date: Sat, 6 Feb 2021 00:27:24 +0000
Subject: [PATCH 17/19] ci: disable fail-fast

---
 .github/workflows/testing_initiative.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/testing_initiative.yml b/.github/workflows/testing_initiative.yml
index 15576f86..4be4b1e7 100644
--- a/.github/workflows/testing_initiative.yml
+++ b/.github/workflows/testing_initiative.yml
@@ -30,6 +30,7 @@ jobs:
   test:
     runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
         python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]

From f405ffab5a1888edb06295cd8378156f60897d44 Mon Sep 17 00:00:00 2001
From: Rossen <2720787+rossengeorgiev@users.noreply.github.com>
Date: Wed, 10 Feb 2021 10:25:19 +0000
Subject: [PATCH 18/19] do not emit chat_message event when its local_echo

Fix for a bug reported in #13
---
 steam/client/builtins/user.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/steam/client/builtins/user.py b/steam/client/builtins/user.py
index 60fd13a4..dbffe508 100644
--- a/steam/client/builtins/user.py
+++ b/steam/client/builtins/user.py
@@ -38,7 +38,7 @@ def __handle_message_incoming(self, msg):
 
     def __handle_message_incoming2(self, msg):
         # new chat
-        if msg.body.chat_entry_type == EChatEntryType.ChatMsg:
+        if msg.body.chat_entry_type == EChatEntryType.ChatMsg and not msg.body.local_echo:
             user = self.get_user(msg.body.steamid_friend)
             self.emit("chat_message", user, msg.body.message)
 

From c16b85c842433d98db8cd1ce48d7cf35ec29173f Mon Sep 17 00:00:00 2001
From: Tom Prince <mozilla@hocat.ca>
Date: Tue, 8 Sep 2020 22:26:01 -0600
Subject: [PATCH 19/19] Port to cryptography.

---
 requirements.txt     |  2 +-
 setup.py             |  2 +-
 steam/core/crypto.py | 79 ++++++++++++++++++++++++++++++++++----------
 3 files changed, 63 insertions(+), 20 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index d5e0306d..08c187d9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 six>=1.10.0
-pycryptodomex>=3.7.0
+cryptography>=3.1
 requests>=2.9.1
 vdf>=3.3
 gevent>=1.3.0
diff --git a/setup.py b/setup.py
index 1c093d53..47ab1a92 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@
 
 install_requires = [
     'six>=1.10',
-    'pycryptodomex>=3.7.0',
+    'cryptography>=3.1',
     'requests>=2.9.1',
     'vdf>=3.3',
     'cachetools>=3.0.0',
diff --git a/steam/core/crypto.py b/steam/core/crypto.py
index 6fb28679..637d95fa 100644
--- a/steam/core/crypto.py
+++ b/steam/core/crypto.py
@@ -1,21 +1,24 @@
 """
 All function in this module take and return :class:`bytes`
 """
+import hashlib
 import sys
+from base64 import b64decode
 from os import urandom as random_bytes
 from struct import pack
-from base64 import b64decode
 
-from Cryptodome.Hash import SHA1, HMAC
-from Cryptodome.PublicKey.RSA import import_key as rsa_import_key, construct as rsa_construct
-from Cryptodome.Cipher import PKCS1_OAEP, PKCS1_v1_5
-from Cryptodome.Cipher import AES as AES
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.padding import PKCS7
+from cryptography.hazmat.primitives.asymmetric import rsa, padding
+from cryptography.hazmat.primitives.hmac import HMAC
+from cryptography.hazmat.primitives.serialization import load_der_public_key
 
 
 class UniverseKey(object):
     """Public keys for Universes"""
 
-    Public = rsa_import_key(b64decode("""
+    Public = load_der_public_key(b64decode("""
 MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDf7BrWLBBmLBc1OhSwfFkRf53T
 2Ct64+AVzRkeRuh7h3SiGEYxqQMUeYKO6UWiSRKpI2hzic9pobFhRr3Bvr/WARvY
 gdTckPv+T1JzZsuVcNfFjrocejN1oWI0Rrtgt4Bo+hOneoo3S57G9F1fOpn5nsQ6
@@ -39,8 +42,14 @@ def generate_session_key(hmac_secret=b''):
     :rtype: :class:`tuple`
     """
     session_key = random_bytes(32)
-    encrypted_session_key = PKCS1_OAEP.new(UniverseKey.Public, SHA1)\
-                                      .encrypt(session_key + hmac_secret)
+    encrypted_session_key = UniverseKey.Public.encrypt(
+        session_key + hmac_secret,
+        padding.OAEP(
+            mgf=padding.MGF1(algorithm=hashes.SHA1()),
+            algorithm=hashes.SHA256(),
+            label=None
+        )
+    )
 
     return (session_key, encrypted_session_key)
 
@@ -49,7 +58,13 @@ def symmetric_encrypt(message, key):
     return symmetric_encrypt_with_iv(message, key, iv)
 
 def symmetric_encrypt_ecb(message, key):
-    return AES.new(key, AES.MODE_ECB).encrypt(pad(message))
+    padder = PKCS7(algorithms.AES.block_size).padder()
+    plaintext = padder.update(message)
+    plaintext += padder.finalize()
+    encryptor = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
+    cyphertext = encryptor.update(plaintext)
+    cyphertext += encryptor.finalize()
+    return cyphertext
 
 def symmetric_encrypt_HMAC(message, key, hmac_secret):
     prefix = random_bytes(3)
@@ -58,11 +73,19 @@ def symmetric_encrypt_HMAC(message, key, hmac_secret):
     return symmetric_encrypt_with_iv(message, key, iv)
 
 def symmetric_encrypt_iv(iv, key):
-    return AES.new(key, AES.MODE_ECB).encrypt(iv)
+    encryptor = Cipher(algorithms.AES(key), modes.ECB()).encryptor()
+    cyphertext = encryptor.update(iv)
+    cyphertext += encryptor.finalize()
+    return cyphertext
 
 def symmetric_encrypt_with_iv(message, key, iv):
     encrypted_iv = symmetric_encrypt_iv(iv, key)
-    cyphertext = AES.new(key, AES.MODE_CBC, iv).encrypt(pad(message))
+    padder = PKCS7(algorithms.AES.block_size).padder()
+    plaintext = padder.update(message)
+    plaintext += padder.finalize()
+    encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
+    cyphertext = encryptor.update(plaintext)
+    cyphertext += encryptor.finalize()
     return encrypted_iv + cyphertext
 
 def symmetric_decrypt(cyphertext, key):
@@ -70,7 +93,13 @@ def symmetric_decrypt(cyphertext, key):
     return symmetric_decrypt_with_iv(cyphertext, key, iv)
 
 def symmetric_decrypt_ecb(cyphertext, key):
-    return unpad(AES.new(key, AES.MODE_ECB).decrypt(cyphertext))
+    decryptor = Cipher(algorithms.AES(key), modes.ECB()).decryptor()
+    plaintext = decryptor.update(cyphertext)
+    plaintext += decryptor.finalize()
+    unpadder = PKCS7(algorithms.AES.block_size).unpadder()
+    message = unpadder.update(plaintext)
+    message += unpadder.finalize()
+    return message
 
 def symmetric_decrypt_HMAC(cyphertext, key, hmac_secret):
     """:raises: :class:`RuntimeError` when HMAC verification fails"""
@@ -85,19 +114,33 @@ def symmetric_decrypt_HMAC(cyphertext, key, hmac_secret):
     return message
 
 def symmetric_decrypt_iv(cyphertext, key):
-    return AES.new(key, AES.MODE_ECB).decrypt(cyphertext[:BS])
+    decryptor = Cipher(algorithms.AES(key), modes.ECB()).decryptor()
+    iv = decryptor.update(cyphertext[:BS])
+    iv += decryptor.finalize()
+    return iv
 
 def symmetric_decrypt_with_iv(cyphertext, key, iv):
-    return unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(cyphertext[BS:]))
+    decryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
+    plaintext = decryptor.update(cyphertext[BS:])
+    plaintext += decryptor.finalize()
+    unpadder = PKCS7(algorithms.AES.block_size).unpadder()
+    message = unpadder.update(plaintext)
+    message += unpadder.finalize()
+    return message
 
 def hmac_sha1(secret, data):
-    return HMAC.new(secret, data, SHA1).digest()
+    h = HMAC(secret, hashes.SHA1())
+    h.update(data)
+    return h.finalize()
 
 def sha1_hash(data):
-    return SHA1.new(data).digest()
+    return hashlib.sha1(data).digest()
 
 def rsa_publickey(mod, exp):
-    return rsa_construct((mod, exp))
+    return rsa.RSAPublicNumbers(e=exp, n=mod).public_key()
 
 def pkcs1v15_encrypt(key, message):
-    return PKCS1_v1_5.new(key).encrypt(message)
+    key.encrypt(
+        message,
+        padding.PKCS1v15,
+    )