├── .gitignore ├── LICENSE ├── README.md ├── examples ├── 0_opensea.io.json ├── 1_wSession.py ├── 2_w3Request.py ├── 3_checker_examples.py ├── check_msg_security.py ├── postmans │ └── 0_opensea.postman_collection.json ├── prepare.py └── sign_example.py ├── flexRequest.pdf ├── flexRequest.png ├── outputs └── 2024-05-03 │ ├── jwt_security │ └── 0_opensea.io.json │ ├── msg_security │ └── 0_opensea.io.json │ ├── msg_sig_security │ └── 0_opensea.io.json │ ├── nonce_security │ └── 0_opensea.io.json │ └── request_items │ └── 0_opensea.io.json ├── requirements.txt ├── test account.txt └── web3_auth_checker ├── __init__.py ├── checkers ├── __init__.py ├── abstract_checker.py ├── jwt_security.py ├── msg_security.py ├── msg_sig_security.py ├── msg_tokenizer.py ├── nonce_security.py └── request_items_checker.py ├── logger.py └── web_request ├── README.md ├── __init__.py ├── web_postman.py ├── web_service.py └── web_session.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web3AuthChecker 2 | Web3AuthChecker is a tool for automatically detecting vulnerabilities through dynamic analysis of Web3 websites. 3 | 4 | 5 | ## Install 6 | - Operating System: Windows 10/11, Ubuntu 20.04 or later 7 | - Python Version: 3.12.3 8 | 9 | ``` 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | 14 | ## Examples 15 | 16 | Examples can be found in the folder [examples](./examples/). 17 | 18 | ### Config Files: 19 | FlexRequest have two config files: 20 | 21 | **Postman file**: 22 | FlexRequest is compatible with postman files, which are used to record the API of each website, such as url, method, headers, body, etc, as shown in [./examples/postmans/0_opensea.postman_collection.json](./examples/postmans/0_opensea.postman_collection.json). 23 | 24 | **Config file**: 25 | The config file records the *request items* and *attack payload*, as shown in [./examples/0_opensea.io.json](./examples/0_opensea.io.json) 26 | 27 | 28 | ### FlexRequest Examples (Section 5.3 in the paper) 29 | 30 | [./examples/1_wSession.py](./examples/1_wSession.py) 31 | 32 | This example shows how FlexRequest can make requests based on the config files and replace parameters (attack payload) in the config files with the local context (*local_conext*). 33 | 34 | ```python 35 | from web3_auth_checker.web_request import WebServicePostman, WebSession 36 | 37 | ws_dir = 'examples' # the folder of the config file 38 | ws_config_file = '0_opensea.io.json' # the config file records the request items and parameters, it also indicates the path of the postman file. 39 | 40 | # Load the config file and postman file 41 | wsp = WebServicePostman(ws_config_file , ws_dir) 42 | 43 | # Create a request Session 44 | ws = WebSession() 45 | 46 | # keys in local_context are used to replace the parameters in the request items 47 | local_context = {'addr':'0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064','private_key': 'f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02'} 48 | 49 | # request the API msg_query with the local_context 50 | ws.request(wsp.get_item('msg_query'),local_context) 51 | 52 | # request the API auth in the config file 53 | ws.request(wsp.get_item('auth')) 54 | 55 | # request the API settings in the config file 56 | ws.request(wsp.get_item('settings')) 57 | 58 | # print the response of the request 59 | for r in ws.after_request_items: 60 | for k,v in r.__dict__.items(): 61 | print(k,':',v) 62 | ``` 63 | 64 | ### Web3AuthChecker Example (Section 5.2 in the paper) 65 | 66 | Web3AuthChecker encapsulates Flexrequest, so you can use Web3Request instead of WebSession to send requests, as shown in [./examples/2_w3Request.py](./examples/2_w3Request.py). 67 | 68 | The following example shows how to execute a Message checker. 69 | A complete example can be found at [./examples/3_checker_examples.py](./examples/3_checker_examples.py). 70 | 71 | ```python 72 | from web3_auth_checker.checkers import MsgChecker 73 | from web3_auth_checker.web_request import WebServicePostman 74 | from web3_auth_checker import logger_init 75 | import logging 76 | 77 | logger = logger_init(logging.ERROR) 78 | 79 | ws_dir = 'examples' 80 | ws_config_file = '0_opensea.io.json' 81 | ws_postman_dir = 'examples/postman' 82 | 83 | # Load the config file and postman file 84 | wsp = WebServicePostman(ws_config_file , ws_dir, ws_postman_dir) 85 | 86 | # Load flexRequest into Message Checker. 87 | c = MsgChecker(wsp, logger) 88 | 89 | # Perform and save 90 | c.check() 91 | c.save_output() 92 | ``` 93 | 94 | ### Output 95 | The results of the checker are stored in the output folder in the format of json files. A json file records the details of the test and the vulnerabilities that exist. Table 2 in the paper summarizes this data. 96 | 97 | Here is a result example: [Msg_security](outputs/2024-05-03/msg_security/0_opensea.io.json). 98 | From the results, we can see that Web3AuthChecker did not find any message-related vulnerabilities in *opensea.io*. 99 | 100 | ```json 101 | { 102 | "detector": "msg_security", 103 | ... 104 | "results": { 105 | "URL_NOT_IN_MSG": false, 106 | "NAME_NOT_IN_MSG": false, 107 | "NONCE_NOT_IN_MSG": false, 108 | "FAKE_MSG": false, 109 | "REPLACE_URL": false, 110 | "REPLACE_NAME": false, 111 | "ADD_statement": false, 112 | } 113 | ... 114 | } 115 | 116 | ``` 117 | 118 | 119 | ## Checker 120 | 121 | Web3AuthChecker has three checkers: 122 | - *[Message Checker](web3_auth_checker\checkers\msg_security.py)* 123 | 124 | - *[Nonce Checker](web3_auth_checker\checkers\nonce_security.py)* 125 | 126 | - *[Signature Checker](web3_auth_checker\checkers\msg_sig_security.py)* 127 | 128 | Details of these checkers can be found in the paper. 129 | Web3AuthChecker also have a *[JSON Web Token Checker](web3_auth_checker\checkers\jwt_security.py)* to detect the vulnerabilities related to the JWT. 130 | 131 | You can add more checkers in the folder [checkers](./web3_auth_checker/checkers/). 132 | 133 | 134 | ## FlexRequest 135 | 136 | Given that the differences in the APIs of each website, existing testing tools or libraries, such as [Postman](https://www.postman.com/) and [Requests](https://github.com/request/request), require the development of separate test scripts for each site, leading to issues of code duplication and maintainability. 137 | To address these challenges, we developed a specialized HTTP library. 138 | 139 | FlexRequest, a Python-based HTTP library, features an automatic replacement mechanism to align with the variations in APIs, providing a flexible and adaptable solution for testing APIs on various websites. 140 | FlexRequest utilizes [cffi\_curl](https://github.com/yifeikong/curl_cffi) for HTTP requests. This Python library creates bindings for [curl-impersonate](https://github.com/lwthiker/curl-impersonate) through the C Foreign Function Interface (CFFI). *curl-impersonate* is a special build of *curl* that can impersonate the four main browsers. 141 | By performing TLS and HTTP handshakes identical to a real browser, *curl-impersonate* ensures that websites do not block requests. 142 | 143 | FlexRequest supports using keys to replace dynamic parameters in the API. Before executing a request, it automatically substitutes these keys with the corresponding values. After receiving the response, FlexRequest retrieves the values at the specified positions according to a predefined configuration, binding them to the corresponding keys. 144 | By managing the values of these keys, developers can perform unified testing across multiple APIs, regardless of their differences. 145 | 146 | Furthermore, FlexRequest maintains a *session context* throughout a session to pass the previous response values between a series of requests. For example, the message obtained from the *Query* response will be stored in the session context and bound to the key *msg*. 147 | When conducting an *Auth* request, FlexRequest automatically populates the request with the value of *msg* from the session context. 148 | 149 | 150 | ### Challenges 151 | To illustrate the challenges of API testing, consider the *QUERY* responses and *AUTH* requests from two different websites as shown in Listing example1 and Listing example2. In the case of galler.io, the *QUERY* returns a message directly, whereas, for element.market, the *QUERY* only returns a nonce. The *AUTH* request parameters also differ in both examples, requiring distinct test scripts for each website. This would typically lead to considerable code duplication and challenges in maintaining the codebase. 152 | 153 | ### Example1: galler.io 154 | ``` 155 | QUERY Response: 156 | {'data':{'auth':{'message':'This is Galler, welcome... 157 | timestamp: 1625468800000'}}} 158 | 159 | AUTH Request: 160 | {method:'POST', url:'https://www.galler.io/api/v1', 161 | headers:{...}, data:{address:'{{ addr }}', 162 | message:'{{ msg }}', signature:'{{ sig }}'}} 163 | ``` 164 | 165 | ### Example2: element.market 166 | ``` 167 | QUERY Response: 168 | {'data':{'auth':{'nonce':'3deca92b'}}} 169 | 170 | AUTH Request: 171 | {method:'POST', url:'https://api.element.market/graphql', 172 | headers:{'x-viewer-addr':'{{ addr }}',...}, data:{message: 173 | '{{ msg }}', nonce:'{{ nonce }}', signature:'{{ sig }}'}} 174 | ``` 175 | 176 | However, FlexRequest's automatic replacement mechanism harmonizes these differences. As seen in Listings example1 and example2, keys such as *addr*, *msg*, etc., are set in the *Auth* requests. By managing the values of these keys, developers can perform unified testing across multiple APIs. 177 | For instance, to test whether a token can still be obtained with an incorrect signature, one needs to set the value of the *sig* key to be empty, which would put an empty signature in all *Auth* requests. 178 | 179 | 180 | FlexRequest 181 | 182 | ### Operation 183 | As shown in Figure, FlexRequest operates in three phases for each request: 184 | 185 | 186 | - **Create.** FlexRequest substitutes keys in the request with values from the *local context*, *session context*, and *inputs*. 187 | - **Execute.** FlexRequest carries out the request using *curl\_cffi*. 188 | - **Destory.** FlexRequest retrieves values from the specified position in the response according to *outputs*, and stores them in the *session context* in a key-value format. 189 | 190 | 191 | A Request Item encapsulates all the pertinent details of a request, including the URL, headers, and so forth. It also includes two special parameters, *inputs* and *outputs*. 192 | Developers can define default values (*inputs*) and return values (*outputs*) in the API's configuration file and set test values in each checker's *local context*. The local context is only valid for the current request, and those values are first filled into the request. 193 | 194 | The key-value replacement function of FlexRequest is very powerful, and the value of a key can even be an executable expression. FlexRequest uses Python's *eval* function to evaluate expressions and return results. Therefore, developers can even simulate front-end JavaScript execution by executable expressions. 195 | Besides, FlexRequest also provides hooks for complex API testing. 196 | 197 | 198 | 199 | 200 | 201 | ## Detail of Config file. 202 | 203 | ```json 204 | { 205 | "schema": "1.0", 206 | "name":"", // The name of the web service 207 | "url":"", // The url of the web service 208 | "postman_file_name":"", // The postman file name 209 | "items":{ 210 | "msg_query":{ // request name, MUST consist with the request name in the postman file. 211 | "perform":"request", // perform type: request, skip 212 | "input":{ // These data will be loaded into the request 213 | "addr": "0x1234...", // this keyword will be fill in the msg. Keywords only fill once, so cannot recurse 214 | "msg":"Please sign this message\n Address:$$ addr $$ Nonce is $$ nonce $$", 215 | }, 216 | "output":{ // The output will be loaded into the next request 217 | /** 218 | json,path:{"kw":[]} 219 | state:200 220 | text 221 | html:html.text 222 | */ 223 | "output":{ 224 | "type":"json", 225 | "path":{ 226 | "nonce":["results","nonce"] // The nonce will be added to the session context 227 | } 228 | }, 229 | }, 230 | "update_request_args":{ 231 | "impersonate": "chrome110", // set the impersonate 232 | "timeout": 10, 233 | "params":{} // You can even update the params 234 | }, 235 | "perform_conf":{} //perform config 236 | }, 237 | 238 | 239 | "auth":{ 240 | "perform":"request", // request, input, skip 241 | "input":{ }, 242 | "output":{ 243 | "type":"json", 244 | "path":{ 245 | "token":["results","accessToken"] // The path of the message 246 | } 247 | }, 248 | "update_request_args":{ 249 | "impersonate": "chrome110", // set the impersonate 250 | }, 251 | "perform_conf":{ 252 | "sign_before_request":true 253 | } //perform config 254 | }, 255 | 256 | "settings":{ 257 | "perform":"request", // request, input, skip 258 | "input":{ }, 259 | "output":{ 260 | "type":"state", 261 | "code": 200 262 | }, 263 | "update_request_args":{ 264 | "impersonate": "chrome110", // set the impersonate 265 | }, 266 | }, 267 | } 268 | } 269 | ``` 270 | 271 | ## Ethical Concerns 272 | For ethical reasons, we will not make the API configuration files of the test websites in the experiment public. 273 | We did not detect vulnerabilities in *opensea.io*, so use its API configuration file as an example. 274 | -------------------------------------------------------------------------------- /examples/0_opensea.io.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": "1.0", 3 | "name":"opensea", 4 | "url":"opensea.io", 5 | "postman_file_name":"0_opensea.postman_collection.json", 6 | "auth_type":"JWT", 7 | "items":{ 8 | "msg_query":{ 9 | "perform":"request", 10 | "update_request_args":{ "impersonate": "chrome110"}, 11 | "input":{}, 12 | "output":{ 13 | "type":"json", 14 | "path":{ 15 | "msg_r":["data","auth","loginMessage"] 16 | } 17 | }, 18 | "response":{"data":{"auth":{"loginMessage":"Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service: https://opensea.io/tos\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nYour authentication status will reset after 24 hours.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\nNonce:\n3deca92b-9c8e-4bac-86cb-893345212441"}}} 19 | }, 20 | "auth":{ 21 | "perform":"request", 22 | "update_request_args":{ "impersonate": "chrome110"}, 23 | "sign_before_request":true, 24 | "input":{ 25 | "msg_body":"$$ eval:context['msg_r'].split('Nonce:')[0] $$", 26 | "nonce":"$$ eval:context['msg_r'].split('Nonce:')[1] $$", 27 | "msg":"$$ eval:context['msg_body'] $$Nonce:$$ eval:context['nonce'] $$" 28 | }, 29 | "output":{ 30 | "type":"json", 31 | "path":{ 32 | "token":["data","auth","login","token"] 33 | } 34 | }, 35 | "response":{"data":{"auth":{"login":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiVlhObGNsUjVjR1U2TkRBeE1qRXhOVE09IiwidXNlcm5hbWUiOiJfX09TX19FY21NbFVQeExic2ZkYmp0Q1RYOXJGU01jUUJLanFNYUdVeE5OZkQ5TVE1eUQzTThIM1FSbm5SOFFjenZVQlZzIiwiYWRkcmVzcyI6IjB4MzZlN2M2ZmViMjBhOTBiMDdmNjM4NjNkMDljYzEyYzRjOWYzOTA2NCIsImlzcyI6Ik9wZW5TZWEiLCJleHAiOjE2Nzk3NTI5OTAsIm9yaWdJYXQiOjE2Nzk2NjY1OTAsImFwaUFjY2VzcyI6Im5vbmUifQ.wgqOR-ZDcreJRygi2JCaN9Lq-ao3ABLvOq5f0TzxVP8","account":{"address":"0x36e7c6feb20a90b07f63863d09cc12c4c9f39064","moonpayKycStatus":"NONE","moonpayKycRejectType":"NONE","isEmployee":false,"id":"QWNjb3VudFR5cGU6MjA2MDIwNDUyNQ=="}}}}} 36 | }, 37 | "settings":{ 38 | "perform":"request", 39 | "update_request_args":{ "impersonate": "chrome110"}, 40 | "input":{}, 41 | "output":{ 42 | "type":"json", 43 | "path":{ 44 | "check_point_1":["data","users","modify"] 45 | } 46 | }, 47 | "response":{"data":{"users":{"modify":{"relayId":"VXNlclR5cGU6NDAxMjExNTM=","id":"VXNlclR5cGU6NDAxMjExNTM="}}}} 48 | }, 49 | "logout":{ 50 | "perform":"skip", 51 | "input":{}, 52 | "output":{}, 53 | "response":{} 54 | } 55 | }, 56 | "check_list":[ 57 | 58 | ] 59 | } -------------------------------------------------------------------------------- /examples/1_wSession.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('./') 3 | 4 | from web3_auth_checker.web_request import WebServicePostman, WebSession 5 | 6 | ws_dir = 'examples' # the folder of the config file 7 | ws_config_file = '0_opensea.io.json' # config file records the request items and parameters 8 | 9 | # Load the config file and postman file 10 | wsp = WebServicePostman(ws_config_file , ws_dir) 11 | 12 | # Create a request Session 13 | ws = WebSession() 14 | 15 | # keys in local_context are used to replace the variables in the request items 16 | local_context = {'addr':'0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064','private_key': 'f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02'} 17 | 18 | # request the API msg_query with the local_context 19 | ws.request(wsp.get_item('msg_query'),local_context) 20 | 21 | # request the API auth in the config file 22 | ws.request(wsp.get_item('auth')) 23 | 24 | # request the API settings in the config file 25 | ws.request(wsp.get_item('settings')) 26 | 27 | # print the response of the request 28 | for r in ws.after_request_items: 29 | for k,v in r.__dict__.items(): 30 | print(k,':',v) 31 | -------------------------------------------------------------------------------- /examples/2_w3Request.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('./') 3 | 4 | from web3_auth_checker.checkers import Web3Request 5 | from web3_auth_checker.web_request import WebServicePostman 6 | from web3_auth_checker import logger_init 7 | import logging 8 | 9 | logger = logger_init(logging.DEBUG) 10 | 11 | ws_dir = 'examples' 12 | ws = WebServicePostman('0_opensea.io.json',ws_dir) 13 | 14 | # print the request items in the config file 15 | for k,v in ws.request_items.items(): 16 | print(k) 17 | 18 | # Web3AuthChecker encapsulates Flexrequest, so you can use Web3Request instead of WebSession to send requests 19 | w3r = Web3Request(ws, logger) 20 | # keys in local_context are used to replace the variables in the request items 21 | local_context = {'addr':'0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064','private_key': 'f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02'} 22 | w3r.request('msg_query', local_context) # request item in the config file 23 | w3r.request('auth') 24 | w3r.request('settings') 25 | w3r.request('logout') -------------------------------------------------------------------------------- /examples/3_checker_examples.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('./') 3 | 4 | from web3_auth_checker.checkers import MsgChecker,MsgSigChecker,NonceChecker,JWTChecker,RequestItemsChecker 5 | from web3_auth_checker.web_request import WebServicePostman 6 | from web3_auth_checker import logger_init 7 | import logging 8 | 9 | logger = logger_init(logging.ERROR) 10 | 11 | ws_dir = 'examples' 12 | w3p = WebServicePostman('0_opensea.io.json',ws_dir) 13 | 14 | # Test the reuquest items in the config file 15 | c0 = RequestItemsChecker(w3p, logger) 16 | c0.check() 17 | c0.save_output() # The result will be saved in the output folder 18 | 19 | 20 | # Message Checker 21 | c1 = MsgChecker(w3p, logger) 22 | c1.check() 23 | c1.save_output() 24 | 25 | # Message Signature Checker 26 | c2 = MsgSigChecker(w3p, logger) 27 | c2.check() 28 | c2.save_output() 29 | 30 | # Nonce Checker 31 | c3 = NonceChecker(w3p, logger) 32 | c3.check() 33 | c3.save_output() 34 | 35 | # JWT Checker 36 | c4 = JWTChecker(w3p, logger) 37 | c4.check() 38 | c4.save_output() 39 | 40 | print('Done') 41 | 42 | -------------------------------------------------------------------------------- /examples/check_msg_security.py: -------------------------------------------------------------------------------- 1 | import prepare 2 | from prepare import web3_dir, get_file_name,run,get_web3s 3 | 4 | from web3_auth_checker.checkers import Web3Request, MsgChecker 5 | from web3_auth_checker.web_request import WebServicePostman 6 | from web3_auth_checker import logger_init 7 | import logging 8 | 9 | logger = logger_init(logging.ERROR) 10 | 11 | # This is a mutiple process example 12 | # This example cannot run because we don't provide the multiple config files. 13 | file_list = get_web3s(0,30) 14 | run(file_list, MsgChecker,logger, 15) 15 | 16 | ''' 17 | for i in range(103,104): 18 | file = get_file_name(str(i)+"_") 19 | if file: 20 | print() 21 | print(file) 22 | w3p = WebServicePostman(file) 23 | c = MsgSigChecker(w3p, logger) 24 | c.check() 25 | c.save_output() 26 | ''' -------------------------------------------------------------------------------- /examples/postmans/0_opensea.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "name": "opensea", 4 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 5 | "description": "", 6 | "_postman_id": "8f99d53e-ab13-4b6c-9194-ae2d4f9ca6f9", 7 | "url":"opensea.io" 8 | }, 9 | "item": [ 10 | { 11 | "request": { 12 | "auth": { 13 | "type": "noauth" 14 | }, 15 | "body": { 16 | "mode": "raw", 17 | "raw": "{\"id\":\"challengeLoginMessageQuery\",\"query\":\"query challengeLoginMessageQuery(\\n $address: AddressScalar!\\n) {\\n auth {\\n loginMessage(address: $address)\\n }\\n}\\n\",\"variables\":{\"address\":\"$$ addr $$\"}}" 18 | }, 19 | "header": [ 20 | { 21 | "key": "x-build-id", 22 | "value": "040afb04751681567a9fb2831b416a56a280139d", 23 | "description": "", 24 | "disabled": false 25 | }, 26 | { 27 | "key": "x-signed-query", 28 | "value": "05649d324b3f3db988d5065ea33599bca390adf00e3f46952dd59ff5cc61e1e0", 29 | "description": "", 30 | "disabled": false 31 | }, 32 | { 33 | "key": "x-viewer-address", 34 | "value": "$$ addr $$", 35 | "description": "", 36 | "disabled": false 37 | }, 38 | { 39 | "key": "accept", 40 | "value": "*/*", 41 | "description": "", 42 | "disabled": false 43 | }, 44 | { 45 | "key": "accept-language", 46 | "value": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 47 | "description": "", 48 | "disabled": false 49 | }, 50 | { 51 | "key": "content-type", 52 | "value": "application/json", 53 | "description": "", 54 | "disabled": false 55 | }, 56 | { 57 | "key": "origin", 58 | "value": "https://opensea.io", 59 | "description": "", 60 | "disabled": false 61 | }, 62 | { 63 | "key": "referer", 64 | "value": "https://opensea.io/", 65 | "description": "", 66 | "disabled": false 67 | }, 68 | { 69 | "key": "sec-ch-ua", 70 | "value": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 71 | "description": "", 72 | "disabled": false 73 | }, 74 | { 75 | "key": "sec-ch-ua-mobile", 76 | "value": "?0", 77 | "description": "", 78 | "disabled": false 79 | }, 80 | { 81 | "key": "sec-ch-ua-platform", 82 | "value": "\"Windows\"", 83 | "description": "", 84 | "disabled": false 85 | }, 86 | { 87 | "key": "x-app-id", 88 | "value": "opensea-web", 89 | "description": "", 90 | "disabled": false 91 | } 92 | ], 93 | "description": "", 94 | "url": { 95 | "raw": "https://opensea.io/__api/graphql/", 96 | "protocol": "https", 97 | "host": [ 98 | "opensea", 99 | "io" 100 | ], 101 | "path": [ 102 | "__api", 103 | "graphql", 104 | "" 105 | ] 106 | }, 107 | "method": "POST" 108 | }, 109 | "name": "msg_query" 110 | }, 111 | { 112 | "request": { 113 | "auth": { 114 | "type": "noauth" 115 | }, 116 | "body": { 117 | "mode": "raw", 118 | "raw": "{\"id\":\"authLoginMutation\",\"query\":\"mutation authLoginMutation(\\n $address: AddressScalar!\\n $message: String!\\n $signature: String!\\n $chain: ChainScalar\\n) {\\n auth {\\n login(address: $address, message: $message, signature: $signature, chain: $chain) {\\n token\\n account {\\n address\\n moonpayKycStatus\\n moonpayKycRejectType\\n isEmployee\\n id\\n }\\n }\\n }\\n}\\n\",\"variables\":{\"address\":\"$$ addr $$\",\"message\":\"$$ msg $$\",\"signature\":\"$$ sig $$\",\"chain\":\"ETHEREUM\"}}" 119 | }, 120 | "header": [ 121 | { 122 | "key": "x-build-id", 123 | "value": "040afb04751681567a9fb2831b416a56a280139d", 124 | "description": "", 125 | "disabled": false 126 | }, 127 | { 128 | "key": "x-signed-query", 129 | "value": "804a717e08ab2f12de3752b428dd9b6fd5d006f26e9f17ec4f4805db69b66e96", 130 | "description": "", 131 | "disabled": false 132 | }, 133 | { 134 | "key": "x-viewer-address", 135 | "value": "$$ addr $$", 136 | "description": "", 137 | "disabled": false 138 | }, 139 | { 140 | "key": "accept", 141 | "value": "*/*", 142 | "description": "", 143 | "disabled": false 144 | }, 145 | { 146 | "key": "accept-language", 147 | "value": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 148 | "description": "", 149 | "disabled": false 150 | }, 151 | { 152 | "key": "content-type", 153 | "value": "application/json", 154 | "description": "", 155 | "disabled": false 156 | }, 157 | { 158 | "key": "origin", 159 | "value": "https://opensea.io", 160 | "description": "", 161 | "disabled": false 162 | }, 163 | { 164 | "key": "referer", 165 | "value": "https://opensea.io/", 166 | "description": "", 167 | "disabled": false 168 | }, 169 | { 170 | "key": "sec-ch-ua", 171 | "value": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 172 | "description": "", 173 | "disabled": false 174 | }, 175 | { 176 | "key": "sec-ch-ua-mobile", 177 | "value": "?0", 178 | "description": "", 179 | "disabled": false 180 | }, 181 | { 182 | "key": "sec-ch-ua-platform", 183 | "value": "\"Windows\"", 184 | "description": "", 185 | "disabled": false 186 | }, 187 | { 188 | "key": "x-app-id", 189 | "value": "opensea-web", 190 | "description": "", 191 | "disabled": false 192 | } 193 | ], 194 | "description": "", 195 | "url": { 196 | "raw": "https://opensea.io/__api/graphql/", 197 | "protocol": "https", 198 | "host": [ 199 | "opensea", 200 | "io" 201 | ], 202 | "path": [ 203 | "__api", 204 | "graphql", 205 | "" 206 | ] 207 | }, 208 | "method": "POST" 209 | }, 210 | "name": "auth" 211 | 212 | }, 213 | { 214 | "request": { 215 | "auth": { 216 | "type": "noauth" 217 | }, 218 | "body": { 219 | "mode": "raw", 220 | "raw": "{\"id\":\"NotificationSettingsMutation\",\"query\":\"mutation NotificationSettingsMutation(\\n $input: UserModifyMutationInput!\\n) {\\n users {\\n modify(input: $input) {\\n relayId\\n id\\n }\\n }\\n}\\n\",\"variables\":{\"input\":{\"bidReceivedEmailsPriceThreshold\":\"5000000000000000\",\"receiveAuctionExpirationEmails\":true,\"receiveBidItemPriceChangeEmails\":true,\"receiveBidReceivedEmails\":true,\"receiveItemSoldEmails\":false,\"receiveNewsletter\":true,\"receiveOutbidEmails\":true,\"receiveOwnedAssetUpdateEmails\":true,\"receivePurchaseEmails\":true,\"receiveReferralEmails\":true}}}" 221 | }, 222 | "header": [ 223 | { 224 | "key": "sec-ch-ua", 225 | "value": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 226 | "description": "", 227 | "disabled": false 228 | }, 229 | { 230 | "key": "sec-ch-ua-mobile", 231 | "value": "?0", 232 | "description": "", 233 | "disabled": false 234 | }, 235 | { 236 | "key": "sec-ch-ua-platform", 237 | "value": "\"Windows\"", 238 | "description": "", 239 | "disabled": false 240 | }, 241 | { 242 | "key": "x-app-id", 243 | "value": "opensea-web", 244 | "description": "", 245 | "disabled": false 246 | }, 247 | { 248 | "key": "x-build-id", 249 | "value": "040afb04751681567a9fb2831b416a56a280139d", 250 | "description": "", 251 | "disabled": false 252 | }, 253 | { 254 | "key": "x-signed-query", 255 | "value": "6e80da18e3a6196e44d0fd4588ed1710595a19778af3fff25324f32e3c72c865", 256 | "description": "", 257 | "disabled": false 258 | }, 259 | { 260 | "key": "x-viewer-address", 261 | "value": "$$ addr $$", 262 | "description": "", 263 | "disabled": false 264 | }, 265 | { 266 | "key": "accept", 267 | "value": "*/*", 268 | "description": "", 269 | "disabled": false 270 | }, 271 | { 272 | "key": "accept-language", 273 | "value": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 274 | "description": "", 275 | "disabled": false 276 | }, 277 | { 278 | "key": "authorization", 279 | "value": "JWT $$ token $$", 280 | "description": "", 281 | "disabled": false 282 | }, 283 | { 284 | "key": "content-type", 285 | "value": "application/json", 286 | "description": "", 287 | "disabled": false 288 | }, 289 | { 290 | "key": "origin", 291 | "value": "https://opensea.io", 292 | "description": "", 293 | "disabled": false 294 | }, 295 | { 296 | "key": "referer", 297 | "value": "https://opensea.io/", 298 | "description": "", 299 | "disabled": false 300 | } 301 | ], 302 | 303 | "description": "", 304 | "url": { 305 | "raw": "https://opensea.io/__api/graphql/", 306 | "protocol": "https", 307 | "host": [ 308 | "opensea", 309 | "io" 310 | ], 311 | "path": [ 312 | "__api", 313 | "graphql", 314 | "" 315 | ] 316 | }, 317 | "method": "POST" 318 | }, 319 | "name": "settings" 320 | } 321 | ] 322 | } -------------------------------------------------------------------------------- /examples/prepare.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append('./') 4 | 5 | 6 | web3_dir = 'examples' 7 | def get_file_name(file_startswith: str) -> str: 8 | for file in os.listdir(web3_dir): 9 | if file.endswith(".json") and file.startswith(file_startswith): 10 | return file 11 | return None 12 | 13 | 14 | ''' 15 | 16 | ''' 17 | from multiprocessing import Process,Queue 18 | from tqdm import tqdm 19 | 20 | def producer(q,file_list): 21 | for c in tqdm(file_list): 22 | q.put(c) 23 | 24 | def consumer(q,checker,logger): 25 | while 1: 26 | c = q.get() 27 | if c: 28 | ch = checker(c,logger) 29 | ch.check() 30 | ch.save_output() 31 | else: 32 | return 33 | 34 | def run(file_list, checker,logger, process_num = 4): 35 | q = Queue(process_num) 36 | p = Process(target=producer,args=(q,file_list,)) 37 | consumers = [Process(target=consumer,args=(q,checker,logger)) for i in range(process_num)] 38 | 39 | tasks = [p] + consumers 40 | for t in tasks: 41 | t.start() 42 | p.join() 43 | for i in range(process_num): q.put(None) 44 | 45 | from web3_auth_checker.web_request import WebServicePostman 46 | def get_web3s(startswith, endswith): 47 | file_list = [] 48 | for i in range(startswith,endswith): 49 | file = get_file_name(str(i)+"_") 50 | if file: 51 | w3p = WebServicePostman(file) 52 | file_list.append(w3p) 53 | 54 | print("File Nums:",len(file_list)) 55 | return file_list 56 | 57 | #file_list = ['1','2','3','4','5','6','7','8','9','10'] 58 | #run(file_list, Checker, 2) -------------------------------------------------------------------------------- /examples/sign_example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('./') 3 | from web3_auth_checker.checkers import TEST_ACCOUNTS 4 | from web3_auth_checker.web_request import sign_msg 5 | 6 | 7 | real_sig = '0xa07c6ccc55e447af2ca9ae34c7b2300555893395fcba0f4c81dfcb703d842f9e4e66ace9cb48b90fad1b5262ebb60eac3bff012733e3dfa362d7088ee4ab5f2e1c' 8 | 9 | #0xeebaC884E95349DD24C6935B5c4E171Ed91c7f50 10 | 11 | msg = 'Welcome to Paragraph! \n\nClick to sign in.\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nHere is your nonce: nyWJ8o92GZ7Dk77KjtGRlw==\n' 12 | sign_msg(msg,TEST_ACCOUNTS[0]['private_key'],True) 13 | -------------------------------------------------------------------------------- /flexRequest.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0scoo1/Web3AuthChecker/7031eeb1da73a6af03ea837724b169bbcdfdf7b2/flexRequest.pdf -------------------------------------------------------------------------------- /flexRequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d0scoo1/Web3AuthChecker/7031eeb1da73a6af03ea837724b169bbcdfdf7b2/flexRequest.png -------------------------------------------------------------------------------- /outputs/2024-05-03/request_items/0_opensea.io.json: -------------------------------------------------------------------------------- 1 | { 2 | "detector": "request_items", 3 | "description": "Check all request items before checking the auth", 4 | "passed": true, 5 | "request_failed": [], 6 | "results": { 7 | "msg_query": true, 8 | "auth": true, 9 | "settings": true, 10 | "logout": true 11 | }, 12 | "after_request_history": [ 13 | { 14 | "name": "msg_query", 15 | "perform": "request", 16 | "input": {}, 17 | "output": { 18 | "type": "json", 19 | "path": { 20 | "msg_r": [ 21 | "data", 22 | "auth", 23 | "loginMessage" 24 | ] 25 | } 26 | }, 27 | "sign": false, 28 | "request_args": { 29 | "method": "POST", 30 | "url": "https://opensea.io/__api/graphql/", 31 | "headers": { 32 | "x-build-id": "040afb04751681567a9fb2831b416a56a280139d", 33 | "x-signed-query": "05649d324b3f3db988d5065ea33599bca390adf00e3f46952dd59ff5cc61e1e0", 34 | "x-viewer-address": "0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064", 35 | "accept": "*/*", 36 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 37 | "content-type": "application/json", 38 | "origin": "https://opensea.io", 39 | "referer": "https://opensea.io/", 40 | "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 41 | "sec-ch-ua-mobile": "?0", 42 | "sec-ch-ua-platform": "\"Windows\"", 43 | "x-app-id": "opensea-web" 44 | }, 45 | "params": {}, 46 | "data": "{\"id\":\"challengeLoginMessageQuery\",\"query\":\"query challengeLoginMessageQuery(\\n $address: AddressScalar!\\n) {\\n auth {\\n loginMessage(address: $address)\\n }\\n}\\n\",\"variables\":{\"address\":\"0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064\"}}", 47 | "timeout": 10, 48 | "impersonate": "chrome110" 49 | }, 50 | "response": [ 51 | { 52 | "url": "https://opensea.io/__api/graphql/", 53 | "content_text": "{\"data\":{\"auth\":{\"loginMessage\":\"Welcome to OpenSea!\\n\\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\\n\\nThis request will not trigger a blockchain transaction or cost any gas fees.\\n\\nWallet address:\\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\\n\\nNonce:\\nb57d563f-2a1a-48c1-8738-07535f48fc8f\"}}}", 54 | "status_code": 200, 55 | "reason": "", 56 | "ok": true, 57 | "cookies": { 58 | "__cf_bm": "JwQHUdzOx_d1yaWN.zc6pvhywca5J.w1g211YH9uEm8-1714848733-1.0.1.1-exGocJtgInIC4iUBEr2C3ZFwc_oF3Wx3C6q.PuejuiQtMZekK3WFoZe1mivFWSRXbYkknib4fLoA.H1y7YYsKw", 59 | "_cfuvid": "zl07DPw6X3M0UwLEZsmb2_J6w3UerPTlqcx4WWIyH5o-1714848733593-0.0.1.1-604800000" 60 | }, 61 | "elapsed": 0.118273, 62 | "encoding": "utf-8", 63 | "charset": "utf-8", 64 | "redirect_count": 0, 65 | "redirect_url": "" 66 | } 67 | ], 68 | "success": true, 69 | "local_context": {}, 70 | "session_context": { 71 | "timestamp10": "1714848731", 72 | "timestamp13": "1714848731198", 73 | "ftime_ia": "2024-05-04T18:52:11.000Z", 74 | "ftime_et": "2024-05-05T18:52:11.000Z", 75 | "private_key": "f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02", 76 | "addr": "0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064", 77 | "addr_low": "0x36e7c6feb20a90b07f63863d09cc12c4c9f39064", 78 | "addr_up": "0x36E7C6FEB20A90B07F63863D09CC12C4C9F39064", 79 | "msg_r": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\nNonce:\nb57d563f-2a1a-48c1-8738-07535f48fc8f" 80 | } 81 | }, 82 | { 83 | "name": "auth", 84 | "perform": "request", 85 | "input": { 86 | "msg_body": "$$ eval:context['msg_r'].split('Nonce:')[0] $$", 87 | "nonce": "$$ eval:context['msg_r'].split('Nonce:')[1] $$", 88 | "msg": "$$ eval:context['msg_body'] $$Nonce:$$ eval:context['nonce'] $$" 89 | }, 90 | "output": { 91 | "type": "json", 92 | "path": { 93 | "token": [ 94 | "data", 95 | "auth", 96 | "login", 97 | "token" 98 | ] 99 | } 100 | }, 101 | "sign": true, 102 | "request_args": { 103 | "method": "POST", 104 | "url": "https://opensea.io/__api/graphql/", 105 | "headers": { 106 | "x-build-id": "040afb04751681567a9fb2831b416a56a280139d", 107 | "x-signed-query": "804a717e08ab2f12de3752b428dd9b6fd5d006f26e9f17ec4f4805db69b66e96", 108 | "x-viewer-address": "0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064", 109 | "accept": "*/*", 110 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 111 | "content-type": "application/json", 112 | "origin": "https://opensea.io", 113 | "referer": "https://opensea.io/", 114 | "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 115 | "sec-ch-ua-mobile": "?0", 116 | "sec-ch-ua-platform": "\"Windows\"", 117 | "x-app-id": "opensea-web" 118 | }, 119 | "params": {}, 120 | "data": "{\"id\":\"authLoginMutation\",\"query\":\"mutation authLoginMutation(\\n $address: AddressScalar!\\n $message: String!\\n $signature: String!\\n $chain: ChainScalar\\n) {\\n auth {\\n login(address: $address, message: $message, signature: $signature, chain: $chain) {\\n token\\n account {\\n address\\n moonpayKycStatus\\n moonpayKycRejectType\\n isEmployee\\n id\\n }\\n }\\n }\\n}\\n\",\"variables\":{\"address\":\"0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064\",\"message\":\"Welcome to OpenSea!\\n\\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\\n\\nThis request will not trigger a blockchain transaction or cost any gas fees.\\n\\nWallet address:\\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\\n\\nNonce:\\nb57d563f-2a1a-48c1-8738-07535f48fc8f\",\"signature\":\"0xb7c5a40bebd8dcc78bcdeb409714eeefa57f4a26cf463ca188f46f5f8cbb867a333cf91453af858ecafe47055a59f49d20b6b47d2e87b572b3a1a16ace3fd6bf1c\",\"chain\":\"ETHEREUM\"}}", 121 | "timeout": 10, 122 | "impersonate": "chrome110" 123 | }, 124 | "response": [ 125 | { 126 | "url": "https://opensea.io/__api/graphql/", 127 | "content_text": "{\"data\":{\"auth\":{\"login\":{\"token\":\"eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiYXJuOmF3czprbXM6dXMtZWFzdC0xOjI1NzExNjk3NTgxODprZXkvbXJrLTI4YWMxODQ3MTM1NTQ4NDQ4NTViNjY2Yjk4MzNkYjMxIn0=.eyJhZGRyZXNzIjogIjB4MzZlN2M2ZmViMjBhOTBiMDdmNjM4NjNkMDljYzEyYzRjOWYzOTA2NCIsICJpc3MiOiAiT3BlblNlYSIsICJleHAiOiAxNzE0OTM1MTMzLCAib3JpZ0lhdCI6IDE3MTQ4NDg3MzQsICJhcGlBY2Nlc3MiOiAibm9uZSJ9.sdkJYxrMeUMnPIOK-LBJc780FP2eQQWK8pggqxWXmsInHELVMOD_CwH1ckVHb_RopojLRedUS-FX4Pw-04EBTaVJvoRrkhFSrDgST0ovShZFH7fN3y8vBWPDOn3kpYcpurX8Y8Y675KNMcjlWOTtN7H2-82-R-lbbW4XxZhfT1vox_Pq8xp_7eT0R4ydrO9o6hcmOIbbNMvx64WyAcTU_7Hwc473ZcE0uMXBVBkJKuczK6SYVZyRbmdZnIa6WScysMR-DZq08aCJOFHJ9HcyfFCYSmWExRSccobgaNjvfChGXlLa7Y1okrf6DBAi0OmvpfTt60xINadP4O8lTMr4aw==\",\"account\":{\"address\":\"0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\",\"moonpayKycStatus\":\"NONE\",\"moonpayKycRejectType\":\"NONE\",\"isEmployee\":false,\"id\":\"QWNjb3VudFR5cGU6MjA2MDIwNDUyNQ==\"}}}}}", 128 | "status_code": 200, 129 | "reason": "", 130 | "ok": true, 131 | "cookies": { 132 | "__cf_bm": "JwQHUdzOx_d1yaWN.zc6pvhywca5J.w1g211YH9uEm8-1714848733-1.0.1.1-exGocJtgInIC4iUBEr2C3ZFwc_oF3Wx3C6q.PuejuiQtMZekK3WFoZe1mivFWSRXbYkknib4fLoA.H1y7YYsKw", 133 | "_cfuvid": "zl07DPw6X3M0UwLEZsmb2_J6w3UerPTlqcx4WWIyH5o-1714848733593-0.0.1.1-604800000" 134 | }, 135 | "elapsed": 0.548922, 136 | "encoding": "utf-8", 137 | "charset": "utf-8", 138 | "redirect_count": 0, 139 | "redirect_url": "" 140 | } 141 | ], 142 | "success": true, 143 | "local_context": {}, 144 | "session_context": { 145 | "timestamp10": "1714848731", 146 | "timestamp13": "1714848731198", 147 | "ftime_ia": "2024-05-04T18:52:11.000Z", 148 | "ftime_et": "2024-05-05T18:52:11.000Z", 149 | "private_key": "f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02", 150 | "addr": "0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064", 151 | "addr_low": "0x36e7c6feb20a90b07f63863d09cc12c4c9f39064", 152 | "addr_up": "0x36E7C6FEB20A90B07F63863D09CC12C4C9F39064", 153 | "msg_r": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\nNonce:\nb57d563f-2a1a-48c1-8738-07535f48fc8f", 154 | "msg_body": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\n", 155 | "nonce": "\nb57d563f-2a1a-48c1-8738-07535f48fc8f", 156 | "msg": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\nNonce:\nb57d563f-2a1a-48c1-8738-07535f48fc8f", 157 | "sig": "0xb7c5a40bebd8dcc78bcdeb409714eeefa57f4a26cf463ca188f46f5f8cbb867a333cf91453af858ecafe47055a59f49d20b6b47d2e87b572b3a1a16ace3fd6bf1c", 158 | "token": "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiYXJuOmF3czprbXM6dXMtZWFzdC0xOjI1NzExNjk3NTgxODprZXkvbXJrLTI4YWMxODQ3MTM1NTQ4NDQ4NTViNjY2Yjk4MzNkYjMxIn0=.eyJhZGRyZXNzIjogIjB4MzZlN2M2ZmViMjBhOTBiMDdmNjM4NjNkMDljYzEyYzRjOWYzOTA2NCIsICJpc3MiOiAiT3BlblNlYSIsICJleHAiOiAxNzE0OTM1MTMzLCAib3JpZ0lhdCI6IDE3MTQ4NDg3MzQsICJhcGlBY2Nlc3MiOiAibm9uZSJ9.sdkJYxrMeUMnPIOK-LBJc780FP2eQQWK8pggqxWXmsInHELVMOD_CwH1ckVHb_RopojLRedUS-FX4Pw-04EBTaVJvoRrkhFSrDgST0ovShZFH7fN3y8vBWPDOn3kpYcpurX8Y8Y675KNMcjlWOTtN7H2-82-R-lbbW4XxZhfT1vox_Pq8xp_7eT0R4ydrO9o6hcmOIbbNMvx64WyAcTU_7Hwc473ZcE0uMXBVBkJKuczK6SYVZyRbmdZnIa6WScysMR-DZq08aCJOFHJ9HcyfFCYSmWExRSccobgaNjvfChGXlLa7Y1okrf6DBAi0OmvpfTt60xINadP4O8lTMr4aw==" 159 | } 160 | }, 161 | { 162 | "name": "settings", 163 | "perform": "request", 164 | "input": {}, 165 | "output": { 166 | "type": "json", 167 | "path": { 168 | "check_point_1": [ 169 | "data", 170 | "users", 171 | "modify" 172 | ] 173 | } 174 | }, 175 | "sign": false, 176 | "request_args": { 177 | "method": "POST", 178 | "url": "https://opensea.io/__api/graphql/", 179 | "headers": { 180 | "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 181 | "sec-ch-ua-mobile": "?0", 182 | "sec-ch-ua-platform": "\"Windows\"", 183 | "x-app-id": "opensea-web", 184 | "x-build-id": "040afb04751681567a9fb2831b416a56a280139d", 185 | "x-signed-query": "6e80da18e3a6196e44d0fd4588ed1710595a19778af3fff25324f32e3c72c865", 186 | "x-viewer-address": "0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064", 187 | "accept": "*/*", 188 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 189 | "authorization": "JWT eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiYXJuOmF3czprbXM6dXMtZWFzdC0xOjI1NzExNjk3NTgxODprZXkvbXJrLTI4YWMxODQ3MTM1NTQ4NDQ4NTViNjY2Yjk4MzNkYjMxIn0=.eyJhZGRyZXNzIjogIjB4MzZlN2M2ZmViMjBhOTBiMDdmNjM4NjNkMDljYzEyYzRjOWYzOTA2NCIsICJpc3MiOiAiT3BlblNlYSIsICJleHAiOiAxNzE0OTM1MTMzLCAib3JpZ0lhdCI6IDE3MTQ4NDg3MzQsICJhcGlBY2Nlc3MiOiAibm9uZSJ9.sdkJYxrMeUMnPIOK-LBJc780FP2eQQWK8pggqxWXmsInHELVMOD_CwH1ckVHb_RopojLRedUS-FX4Pw-04EBTaVJvoRrkhFSrDgST0ovShZFH7fN3y8vBWPDOn3kpYcpurX8Y8Y675KNMcjlWOTtN7H2-82-R-lbbW4XxZhfT1vox_Pq8xp_7eT0R4ydrO9o6hcmOIbbNMvx64WyAcTU_7Hwc473ZcE0uMXBVBkJKuczK6SYVZyRbmdZnIa6WScysMR-DZq08aCJOFHJ9HcyfFCYSmWExRSccobgaNjvfChGXlLa7Y1okrf6DBAi0OmvpfTt60xINadP4O8lTMr4aw==", 190 | "content-type": "application/json", 191 | "origin": "https://opensea.io", 192 | "referer": "https://opensea.io/" 193 | }, 194 | "params": {}, 195 | "data": "{\"id\":\"NotificationSettingsMutation\",\"query\":\"mutation NotificationSettingsMutation(\\n $input: UserModifyMutationInput!\\n) {\\n users {\\n modify(input: $input) {\\n relayId\\n id\\n }\\n }\\n}\\n\",\"variables\":{\"input\":{\"bidReceivedEmailsPriceThreshold\":\"5000000000000000\",\"receiveAuctionExpirationEmails\":true,\"receiveBidItemPriceChangeEmails\":true,\"receiveBidReceivedEmails\":true,\"receiveItemSoldEmails\":false,\"receiveNewsletter\":true,\"receiveOutbidEmails\":true,\"receiveOwnedAssetUpdateEmails\":true,\"receivePurchaseEmails\":true,\"receiveReferralEmails\":true}}}", 196 | "timeout": 10, 197 | "impersonate": "chrome110" 198 | }, 199 | "response": [ 200 | { 201 | "url": "https://opensea.io/__api/graphql/", 202 | "content_text": "{\"data\":{\"users\":{\"modify\":{\"relayId\":\"VXNlclR5cGU6NDAxMjExNTM=\",\"id\":\"VXNlclR5cGU6NDAxMjExNTM=\"}}}}", 203 | "status_code": 200, 204 | "reason": "", 205 | "ok": true, 206 | "cookies": { 207 | "__cf_bm": "JwQHUdzOx_d1yaWN.zc6pvhywca5J.w1g211YH9uEm8-1714848733-1.0.1.1-exGocJtgInIC4iUBEr2C3ZFwc_oF3Wx3C6q.PuejuiQtMZekK3WFoZe1mivFWSRXbYkknib4fLoA.H1y7YYsKw", 208 | "_cfuvid": "zl07DPw6X3M0UwLEZsmb2_J6w3UerPTlqcx4WWIyH5o-1714848733593-0.0.1.1-604800000" 209 | }, 210 | "elapsed": 0.249231, 211 | "encoding": "utf-8", 212 | "charset": "utf-8", 213 | "redirect_count": 0, 214 | "redirect_url": "" 215 | } 216 | ], 217 | "success": true, 218 | "local_context": {}, 219 | "session_context": { 220 | "timestamp10": "1714848731", 221 | "timestamp13": "1714848731198", 222 | "ftime_ia": "2024-05-04T18:52:11.000Z", 223 | "ftime_et": "2024-05-05T18:52:11.000Z", 224 | "private_key": "f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02", 225 | "addr": "0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064", 226 | "addr_low": "0x36e7c6feb20a90b07f63863d09cc12c4c9f39064", 227 | "addr_up": "0x36E7C6FEB20A90B07F63863D09CC12C4C9F39064", 228 | "msg_r": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\nNonce:\nb57d563f-2a1a-48c1-8738-07535f48fc8f", 229 | "msg_body": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\n", 230 | "nonce": "\nb57d563f-2a1a-48c1-8738-07535f48fc8f", 231 | "msg": "Welcome to OpenSea!\n\nClick to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\nWallet address:\n0x36e7c6feb20a90b07f63863d09cc12c4c9f39064\n\nNonce:\nb57d563f-2a1a-48c1-8738-07535f48fc8f", 232 | "sig": "0xb7c5a40bebd8dcc78bcdeb409714eeefa57f4a26cf463ca188f46f5f8cbb867a333cf91453af858ecafe47055a59f49d20b6b47d2e87b572b3a1a16ace3fd6bf1c", 233 | "token": "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiYXJuOmF3czprbXM6dXMtZWFzdC0xOjI1NzExNjk3NTgxODprZXkvbXJrLTI4YWMxODQ3MTM1NTQ4NDQ4NTViNjY2Yjk4MzNkYjMxIn0=.eyJhZGRyZXNzIjogIjB4MzZlN2M2ZmViMjBhOTBiMDdmNjM4NjNkMDljYzEyYzRjOWYzOTA2NCIsICJpc3MiOiAiT3BlblNlYSIsICJleHAiOiAxNzE0OTM1MTMzLCAib3JpZ0lhdCI6IDE3MTQ4NDg3MzQsICJhcGlBY2Nlc3MiOiAibm9uZSJ9.sdkJYxrMeUMnPIOK-LBJc780FP2eQQWK8pggqxWXmsInHELVMOD_CwH1ckVHb_RopojLRedUS-FX4Pw-04EBTaVJvoRrkhFSrDgST0ovShZFH7fN3y8vBWPDOn3kpYcpurX8Y8Y675KNMcjlWOTtN7H2-82-R-lbbW4XxZhfT1vox_Pq8xp_7eT0R4ydrO9o6hcmOIbbNMvx64WyAcTU_7Hwc473ZcE0uMXBVBkJKuczK6SYVZyRbmdZnIa6WScysMR-DZq08aCJOFHJ9HcyfFCYSmWExRSccobgaNjvfChGXlLa7Y1okrf6DBAi0OmvpfTt60xINadP4O8lTMr4aw==", 234 | "check_point_1": { 235 | "relayId": "VXNlclR5cGU6NDAxMjExNTM=", 236 | "id": "VXNlclR5cGU6NDAxMjExNTM=" 237 | } 238 | } 239 | } 240 | ], 241 | "before_request_history": [ 242 | { 243 | "name": "msg_query", 244 | "perform": "request", 245 | "input": {}, 246 | "output": { 247 | "type": "json", 248 | "path": { 249 | "msg_r": [ 250 | "data", 251 | "auth", 252 | "loginMessage" 253 | ] 254 | } 255 | }, 256 | "sign": false, 257 | "request_args": { 258 | "method": "POST", 259 | "url": "https://opensea.io/__api/graphql/", 260 | "headers": { 261 | "x-build-id": "040afb04751681567a9fb2831b416a56a280139d", 262 | "x-signed-query": "05649d324b3f3db988d5065ea33599bca390adf00e3f46952dd59ff5cc61e1e0", 263 | "x-viewer-address": "$$ addr $$", 264 | "accept": "*/*", 265 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 266 | "content-type": "application/json", 267 | "origin": "https://opensea.io", 268 | "referer": "https://opensea.io/", 269 | "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 270 | "sec-ch-ua-mobile": "?0", 271 | "sec-ch-ua-platform": "\"Windows\"", 272 | "x-app-id": "opensea-web" 273 | }, 274 | "params": {}, 275 | "data": "{\"id\":\"challengeLoginMessageQuery\",\"query\":\"query challengeLoginMessageQuery(\\n $address: AddressScalar!\\n) {\\n auth {\\n loginMessage(address: $address)\\n }\\n}\\n\",\"variables\":{\"address\":\"$$ addr $$\"}}", 276 | "timeout": 10, 277 | "impersonate": "chrome110" 278 | }, 279 | "response": [ 280 | null 281 | ], 282 | "success": null, 283 | "local_context": {}, 284 | "session_context": {} 285 | }, 286 | { 287 | "name": "auth", 288 | "perform": "request", 289 | "input": { 290 | "msg_body": "$$ eval:context['msg_r'].split('Nonce:')[0] $$", 291 | "nonce": "$$ eval:context['msg_r'].split('Nonce:')[1] $$", 292 | "msg": "$$ eval:context['msg_body'] $$Nonce:$$ eval:context['nonce'] $$" 293 | }, 294 | "output": { 295 | "type": "json", 296 | "path": { 297 | "token": [ 298 | "data", 299 | "auth", 300 | "login", 301 | "token" 302 | ] 303 | } 304 | }, 305 | "sign": true, 306 | "request_args": { 307 | "method": "POST", 308 | "url": "https://opensea.io/__api/graphql/", 309 | "headers": { 310 | "x-build-id": "040afb04751681567a9fb2831b416a56a280139d", 311 | "x-signed-query": "804a717e08ab2f12de3752b428dd9b6fd5d006f26e9f17ec4f4805db69b66e96", 312 | "x-viewer-address": "$$ addr $$", 313 | "accept": "*/*", 314 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 315 | "content-type": "application/json", 316 | "origin": "https://opensea.io", 317 | "referer": "https://opensea.io/", 318 | "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 319 | "sec-ch-ua-mobile": "?0", 320 | "sec-ch-ua-platform": "\"Windows\"", 321 | "x-app-id": "opensea-web" 322 | }, 323 | "params": {}, 324 | "data": "{\"id\":\"authLoginMutation\",\"query\":\"mutation authLoginMutation(\\n $address: AddressScalar!\\n $message: String!\\n $signature: String!\\n $chain: ChainScalar\\n) {\\n auth {\\n login(address: $address, message: $message, signature: $signature, chain: $chain) {\\n token\\n account {\\n address\\n moonpayKycStatus\\n moonpayKycRejectType\\n isEmployee\\n id\\n }\\n }\\n }\\n}\\n\",\"variables\":{\"address\":\"$$ addr $$\",\"message\":\"$$ msg $$\",\"signature\":\"$$ sig $$\",\"chain\":\"ETHEREUM\"}}", 325 | "timeout": 10, 326 | "impersonate": "chrome110" 327 | }, 328 | "response": [ 329 | null 330 | ], 331 | "success": null, 332 | "local_context": {}, 333 | "session_context": {} 334 | }, 335 | { 336 | "name": "settings", 337 | "perform": "request", 338 | "input": {}, 339 | "output": { 340 | "type": "json", 341 | "path": { 342 | "check_point_1": [ 343 | "data", 344 | "users", 345 | "modify" 346 | ] 347 | } 348 | }, 349 | "sign": false, 350 | "request_args": { 351 | "method": "POST", 352 | "url": "https://opensea.io/__api/graphql/", 353 | "headers": { 354 | "sec-ch-ua": "\"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108\"", 355 | "sec-ch-ua-mobile": "?0", 356 | "sec-ch-ua-platform": "\"Windows\"", 357 | "x-app-id": "opensea-web", 358 | "x-build-id": "040afb04751681567a9fb2831b416a56a280139d", 359 | "x-signed-query": "6e80da18e3a6196e44d0fd4588ed1710595a19778af3fff25324f32e3c72c865", 360 | "x-viewer-address": "$$ addr $$", 361 | "accept": "*/*", 362 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5", 363 | "authorization": "JWT $$ token $$", 364 | "content-type": "application/json", 365 | "origin": "https://opensea.io", 366 | "referer": "https://opensea.io/" 367 | }, 368 | "params": {}, 369 | "data": "{\"id\":\"NotificationSettingsMutation\",\"query\":\"mutation NotificationSettingsMutation(\\n $input: UserModifyMutationInput!\\n) {\\n users {\\n modify(input: $input) {\\n relayId\\n id\\n }\\n }\\n}\\n\",\"variables\":{\"input\":{\"bidReceivedEmailsPriceThreshold\":\"5000000000000000\",\"receiveAuctionExpirationEmails\":true,\"receiveBidItemPriceChangeEmails\":true,\"receiveBidReceivedEmails\":true,\"receiveItemSoldEmails\":false,\"receiveNewsletter\":true,\"receiveOutbidEmails\":true,\"receiveOwnedAssetUpdateEmails\":true,\"receivePurchaseEmails\":true,\"receiveReferralEmails\":true}}}", 370 | "timeout": 10, 371 | "impersonate": "chrome110" 372 | }, 373 | "response": [ 374 | null 375 | ], 376 | "success": null, 377 | "local_context": {}, 378 | "session_context": {} 379 | }, 380 | { 381 | "name": "logout", 382 | "perform": "skip", 383 | "input": {}, 384 | "output": {}, 385 | "sign": false, 386 | "request_args": {}, 387 | "response": [ 388 | null 389 | ], 390 | "success": null, 391 | "local_context": {}, 392 | "session_context": {} 393 | } 394 | ] 395 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eth_account == 0.8.0 2 | curl_cffi == 0.3.7 3 | colorlog == 6.7.0 4 | tqdm == 4.62.3 5 | setuptools == 69.2.0 -------------------------------------------------------------------------------- /test account.txt: -------------------------------------------------------------------------------- 1 | //These accounts are only for login testing, never send cryptocurrency to them 2 | 3 | Phrase: 4 | fragile sound season that sock citizen jungle shrug song fall leave useless 5 | 6 | TEST_ACCOUNTS = [ 7 | {'private_key': 'f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02', 8 | 'addr': '0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064'}, 9 | 10 | {'private_key': '32dfebf1b058471b80abc5434ee7229a19c870b2c85797afdbf1fb21dccaf3cd', 11 | 'addr': '0x3BB5DdC2703B0C2a82952f25c521BE95dC9dee37'}, 12 | 13 | {'private_key': '070175b068eeda71a5fab6dbd0bab9ee4ea3123729278f9ea7c192e408ac4385', 14 | 'addr': '0xeebaC884E95349DD24C6935B5c4E171Ed91c7f50'}, 15 | ] -------------------------------------------------------------------------------- /web3_auth_checker/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import logger_init -------------------------------------------------------------------------------- /web3_auth_checker/checkers/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract_checker import AbstractChecker, Web3Request, TEST_ACCOUNTS 2 | from .request_items_checker import RequestItemsChecker 3 | from .msg_sig_security import MsgSigChecker 4 | from .jwt_security import JWTChecker 5 | from .msg_security import MsgChecker 6 | from .nonce_security import NonceChecker -------------------------------------------------------------------------------- /web3_auth_checker/checkers/abstract_checker.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import json 3 | import os 4 | from logging import Logger 5 | from typing import Optional, List, TYPE_CHECKING, Dict, Union, Callable 6 | import copy 7 | import time 8 | 9 | from web3_auth_checker.web_request import WebSession, WebService, RequestItem 10 | 11 | 12 | TEST_ACCOUNTS = [ 13 | {'private_key': 'f78411d5886f5ded63cd304b9b56dd87b05ce0922223e87b4927cc56bfaa7b02', 14 | 'addr': '0x36E7C6FeB20A90b07F63863D09cC12C4c9f39064', 15 | 'addr_low': '0x36e7c6feb20a90b07f63863d09cc12c4c9f39064', 16 | 'addr_up': '0x36E7C6FEB20A90B07F63863D09CC12C4C9F39064' 17 | }, 18 | 19 | {'private_key': '32dfebf1b058471b80abc5434ee7229a19c870b2c85797afdbf1fb21dccaf3cd', 20 | 'addr': '0x3BB5DdC2703B0C2a82952f25c521BE95dC9dee37', 21 | 'addr_low': '0x3bb5ddc2703b0c2a82952f25c521be95dc9dee37', 22 | 'addr_up': '0x3BB5DDC2703B0C2A82952F25C521BE95DC9DEE37' 23 | }, 24 | 25 | {'private_key': '070175b068eeda71a5fab6dbd0bab9ee4ea3123729278f9ea7c192e408ac4385', 26 | 'addr': '0xeebaC884E95349DD24C6935B5c4E171Ed91c7f50', 27 | 'addr_low': '0xeebac884e95349dd24c6935b5c4e171ed91c7f50', 28 | 'addr_up': '0xEEBAC884E95349DD24C6935B5C4E171ED91C7F50' 29 | }, 30 | ] 31 | 32 | class Web3Request(): 33 | def __init__( 34 | self, 35 | web3:WebService, 36 | logger: Logger, 37 | account_index = 0 38 | ) -> None: 39 | 40 | self.web3 = copy.deepcopy(web3) 41 | self.logger = logger 42 | 43 | if account_index >= len(TEST_ACCOUNTS) or account_index < 0: 44 | raise ValueError(f'account_index {account_index} is out of range') 45 | 46 | # Construct the session context 47 | timestamp = time.time() 48 | session_context = { 49 | # set timestamp in session_context 50 | "timestamp10" : str(int(timestamp)), 51 | "timestamp13" : str(int(timestamp*1000)), 52 | "ftime_ia": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime(timestamp)), 53 | "ftime_et": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime(timestamp+86400)), 54 | } 55 | session_context.update(TEST_ACCOUNTS[account_index]) 56 | 57 | self.session = WebSession(session_context) # add the acount to the session context 58 | 59 | def _request(self, request_fun, item_name, local_context = None, context_middleware = None): 60 | ''' 61 | Perform a request 62 | ''' 63 | # Get the request item 64 | 65 | item = self.web3.get_item(item_name) 66 | if item is None: 67 | raise ValueError(f'No request item named {item_name}') 68 | 69 | self.logger.log(41,f'Web3Request: {self.web3.name} - {item_name} - {item.perform}') 70 | 71 | success = request_fun(item, local_context, context_middleware) 72 | 73 | if success and item.perform != 'skip': 74 | self.logger.debug(f'Web3Request: {self.web3.name} - {item_name} - {success} \n {item.request_args}') 75 | self.logger.debug(f'Response: {item.response.status_code}: {item.response.text}') 76 | 77 | if not success: 78 | if item.response is not None: 79 | self.logger.warn(f'Web3Request: {self.web3.name} - {item_name} - Response: {item.response.status_code}: {item.response.text}') 80 | self.logger.warn(f'Web3Request: {self.web3.name} - {item_name}:\n {item.session_context}') 81 | self.logger.debug(f'Web3Request: {self.web3.name} - {item_name} - {success} \n {item.request_args}') 82 | 83 | return success 84 | 85 | def request(self, item_name, local_context = None, context_middleware = None): 86 | return self._request(self.session.request, item_name, local_context, context_middleware) 87 | 88 | def request_again(self, item_name): 89 | return self._request(self.session.request_again, item_name) 90 | 91 | def get_item(self, item_name): 92 | return self.web3.get_item(item_name) 93 | 94 | 95 | 96 | class AbstractChecker(abc.ABC): 97 | NAME = "Abstract Checker" 98 | DESCRIPTION = "" 99 | 100 | REQUEST_ITEMS = ['msg_query','auth','settings','logout'] 101 | 102 | def __init__( 103 | self, 104 | web_service: WebService, 105 | logger: Logger, 106 | ): 107 | self.web3 = web_service 108 | self.logger = logger 109 | self.logger.name = self.NAME 110 | 111 | self.passed = False 112 | self.request_failed = [] 113 | self.results = [] 114 | 115 | self.w3Requests = [] 116 | 117 | 118 | @abc.abstractmethod 119 | def _check(self): 120 | ''' 121 | This is the function to be implemented by the implementing class. 122 | ''' 123 | pass 124 | 125 | def check(self): 126 | self._check() 127 | if self.passed: 128 | self.logger.info(f'{self.web3.name} - \033[1;32mPASS\033[0m') 129 | else: 130 | self.logger.warn(f'{self.web3.name} - \033[1;31mFAIL\033[0m') 131 | 132 | return self.passed 133 | 134 | def create_web3_request(self, account_index = 0): 135 | ''' 136 | Create a web3 session with the account index 137 | ''' 138 | w3r = Web3Request(self.web3, self.logger, account_index) 139 | self.w3Requests.append(w3r) 140 | return w3r 141 | 142 | def request(self, w3r, item_name, local_context = None, context_middleware = None): 143 | r = False 144 | try: 145 | r = w3r.request(item_name, local_context, context_middleware) 146 | except Exception as e: 147 | #pass 148 | print(e) 149 | 150 | if not r: 151 | self.request_failed.append(item_name) 152 | return False 153 | return True 154 | 155 | def request_again(self, w3r, item_name): 156 | r = False 157 | try: 158 | r = w3r.request_again(item_name) 159 | except Exception as e: 160 | pass 161 | if not r: 162 | self.request_failed.append(item_name) 163 | return False 164 | return True 165 | 166 | def get_item(self, item_name): 167 | return self.web3.get_item(item_name) 168 | @property 169 | def output(self): 170 | ''' 171 | Output the results of the detector 172 | ''' 173 | before_request_history = [] 174 | after_request_history = [] 175 | 176 | for w3r in self.w3Requests: 177 | for item in w3r.session.before_request_items: 178 | before_request_history.append(item.__dict__) 179 | for item in w3r.session.after_request_items: 180 | after_request_history.append(item.__dict__) 181 | 182 | return { 183 | 'detector': self.NAME, 184 | 'description': self.DESCRIPTION, 185 | 'passed': self.passed, 186 | 'request_failed': self.request_failed, 187 | 'results': self.results, 188 | 'after_request_history': after_request_history, 189 | 'before_request_history': before_request_history, 190 | } 191 | 192 | def save_output(self, output_dir: str = './outputs'): 193 | ''' 194 | Save the output of the detector to the output directory 195 | ''' 196 | output_dir = os.path.join(output_dir, time.strftime("%Y-%m-%d")) 197 | 198 | detector_dir = os.path.join(output_dir, self.NAME) 199 | 200 | if not os.path.exists(detector_dir): 201 | os.makedirs(detector_dir) 202 | 203 | 204 | file_name = self.web3.webservice_filename 205 | 206 | print(file_name) 207 | file_path = os.path.join(detector_dir, file_name) 208 | with open(file_path, 'w',encoding="UTF-8") as f: 209 | json.dump(self.output, f, indent=4) 210 | -------------------------------------------------------------------------------- /web3_auth_checker/checkers/jwt_security.py: -------------------------------------------------------------------------------- 1 | from .abstract_checker import AbstractChecker 2 | import base64 3 | import time 4 | import json 5 | 6 | SLEEP_TIME = 61 7 | class JWTChecker(AbstractChecker): 8 | NAME = "jwt_security" 9 | DESCRIPTION = "Json Web Token Checker" 10 | 11 | def _check(self): 12 | self.results = { 13 | "TOKEN_TYPE": None, # 0: None, 1: JWT 14 | "TOKEN":None, 15 | "TOKEN_LOCATION": None, # 0: None, 1: Header, 2: Cookie 16 | 17 | "DECODED_TOKEN":None, 18 | "REQUEST_TIME":None, 19 | "ISSUED_AT":None, 20 | "EXPIRES_IN":None, 21 | "DURATION":None, # exp - iat || exp - request_time 22 | "EXPIRES":None, # expires 23 | 24 | "VALID_AFTER_LOGOUT":None, # 0: False, 1: True Still valid after logout 25 | "VALID_AT_SAME_TIME":None, # 0: False, 1: True Still valid at the same time 26 | "VALID_AFTER_FIRST_LOGOUT":None, 27 | #"TOKEN_INVALID_TYPE":None, # The ways to invalidate the token (logout, new_login, change password, etc) 28 | 29 | 30 | "NOT_CHECK_TOKEN_IS_EMPTY": None, # 0: False, 1: True ONLY FOR TOKEN TYPE 31 | 32 | "NOT_CHECK_JWT_HEAD_IS_NONE": None, 33 | "NOT_CHECK_JWT_SIG": None, 34 | "NOT_CHECK_JWT_SIG_IS_EMPTY": None, 35 | } 36 | 37 | if self.web3.auth_type.upper() == "SIG": 38 | self.results['TOKEN_TYPE'] = 'SIG' 39 | self.passed = True 40 | return 41 | 42 | 43 | if self.web3.auth_type.upper() == "TOKEN": 44 | self.results['TOKEN_TYPE'] = 'TOKEN' 45 | self._check_info() 46 | #NOT_CHECK_TOKEN_IS_EMPTY 47 | self._check_token_is_empty() 48 | 49 | if self.web3.auth_type.upper() == "JWT": 50 | self.results['TOKEN_TYPE'] = 'JWT' 51 | self._check_info() 52 | # NOT_CHECK_JWT_IS_EMPTY, NOT_CHECK_JWT_SIG, NOT_CHECK_JWT_SIG_IS_EMPTY 53 | if self.results['TOKEN_LOCATION'] == 'Header': 54 | self._check_jwt() 55 | 56 | self.passed = True 57 | print(self.results) 58 | 59 | 60 | def _check_info(self): 61 | w3r1 = self.create_web3_request() 62 | self.results['REQUEST_TIME'] = int(w3r1.session.session_context['timestamp10']) 63 | for item in ['msg_query', 'auth', 'settings']: 64 | if not self.request(w3r1, item): 65 | self.passed = False 66 | return 67 | 68 | # Find the token 69 | if "token" in w3r1.session.session_context: 70 | self.results['TOKEN'] = w3r1.session.session_context['token'] 71 | self.results['TOKEN_LOCATION'] = 'Header' 72 | if "expires" in w3r1.session.session_context: 73 | self.results['EXPIRES'] = w3r1.session.session_context['expires'] 74 | else: # search in cookies 75 | s_cookies = w3r1.session.session.cookies 76 | for c_name in s_cookies: 77 | c_value = s_cookies.get(c_name) 78 | if c_value.startswith('eyJ'): 79 | self.results['TOKEN'] = c_value 80 | self.results['TOKEN_LOCATION'] = 'Cookie' 81 | break 82 | 83 | # Do not find the token 84 | if self.results['TOKEN'] is None: 85 | self.results['TOKEN'] = "DO NOT FIND THE TOKEN" 86 | self.passed = False 87 | return 88 | 89 | # JWT info 90 | self._decode_jwt() 91 | 92 | # VALID_AFTER_LOGOUT 93 | self.results['VALID_AFTER_LOGOUT'] = False 94 | self.request(w3r1, 'logout') 95 | if self.request_again(w3r1,"settings"): 96 | self.results['VALID_AFTER_LOGOUT'] = True 97 | 98 | 99 | #VALID_AT_SAME_TIME 100 | self.results['VALID_AT_SAME_TIME'] = True 101 | time.sleep(SLEEP_TIME) 102 | w3r2 = self.create_web3_request() 103 | 104 | for item in ['msg_query', 'auth', 'settings']: 105 | if not self.request(w3r2, item): 106 | self.passed = False 107 | return 108 | 109 | time.sleep(int(SLEEP_TIME/2)) 110 | w3r3 = self.create_web3_request() 111 | for item in ['msg_query', 'auth', 'settings']: 112 | if not self.request(w3r3, item): 113 | self.results['VALID_AT_SAME_TIME'] = False 114 | 115 | #VALID_AFTER_FIRST_LOGOUT 116 | self.results['VALID_AFTER_FIRST_LOGOUT'] = False 117 | self.request(w3r2, 'logout') 118 | 119 | if self.request_again(w3r3,"settings"): 120 | self.results['VALID_AFTER_FIRST_LOGOUT'] = True 121 | 122 | 123 | def _decode_jwt(self): 124 | if self.results['TOKEN_TYPE'] != 'JWT': 125 | return 126 | token = self.results['TOKEN'] 127 | 128 | tokens = token.split('.') 129 | if len(tokens) != 3: 130 | return 131 | 132 | header = tokens[0] 133 | payload = tokens[1] 134 | #signature = tokens[2] 135 | 136 | 137 | header = safe_base64_decode(header) 138 | payload = safe_base64_decode(payload) 139 | 140 | self.results['DECODED_TOKEN'] = header + payload 141 | 142 | ts = [] 143 | for v in json.loads(payload).values(): 144 | if type(v) == int: 145 | if len(str(v)) == 13: 146 | ts.append(int(v/1000)) 147 | elif len(str(v)) == 10: 148 | ts.append(v) 149 | 150 | if len(ts) == 1: 151 | self.results['EXPIRES_IN'] = ts[0] 152 | self.results['DURATION'] = self.results['EXPIRES_IN'] - self.results['REQUEST_TIME'] 153 | elif len(ts) == 2: 154 | if ts[0] > ts[1]: 155 | self.results['ISSUED_AT'] = ts[1] 156 | self.results['EXPIRES_IN'] = ts[0] 157 | else: 158 | self.results['ISSUED_AT'] = ts[0] 159 | self.results['EXPIRES_IN'] = ts[1] 160 | self.results['DURATION'] = self.results['EXPIRES_IN'] - self.results['ISSUED_AT'] 161 | 162 | def _check_token_is_empty(self): 163 | self.results['NOT_CHECK_TOKEN_IS_EMPTY'] = False 164 | w3r = self.create_web3_request() 165 | for item in ['msg_query', 'auth']: 166 | if not self.request(w3r, item): 167 | self.passed = False 168 | return 169 | 170 | if self.request(w3r, 'settings',{'token':''}): 171 | self.results['NOT_CHECK_TOKEN_IS_EMPTY'] = True 172 | 173 | 174 | def _check_jwt(self): 175 | time.sleep(SLEEP_TIME) 176 | self._check_jwt_head_is_none() 177 | time.sleep(SLEEP_TIME) 178 | self._check_jwt_sig() 179 | time.sleep(SLEEP_TIME) 180 | self._check_jwt_sig_is_empty() 181 | 182 | def _check_jwt_head_is_none(self): 183 | self.results['NOT_CHECK_JWT_HEAD_IS_NONE'] = False 184 | w3r = self.create_web3_request() 185 | for item in ['msg_query', 'auth']: 186 | if not self.request(w3r, item): 187 | self.passed = False 188 | return 189 | 190 | token = w3r.session.session_context['token'] 191 | tokens = token.split('.') 192 | if len(tokens) != 3: 193 | return 194 | 195 | fake_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.'+tokens[1]+'.'+tokens[2] 196 | if self.request(w3r, 'settings',{'token':fake_token}): 197 | self.results['NOT_CHECK_JWT_HEAD_IS_NONE'] = True 198 | 199 | def _check_jwt_sig(self): 200 | self.results['NOT_CHECK_JWT_SIG'] = False 201 | w3r = self.create_web3_request() 202 | for item in ['msg_query', 'auth']: 203 | if not self.request(w3r, item): 204 | self.passed = False 205 | return 206 | 207 | token = w3r.session.session_context['token'] 208 | tokens = token.split('.') 209 | if len(tokens) != 3: 210 | return 211 | 212 | fake_token = tokens[0]+'.'+tokens[1]+'.AAAAAAAAAAAyjsczisuPXzAPVzPD0CnCjkxDBfaQMPg' 213 | if self.request(w3r, 'settings',{'token':fake_token}): 214 | self.results['NOT_CHECK_JWT_SIG'] = True 215 | 216 | def _check_jwt_sig_is_empty(self): 217 | self.results['NOT_CHECK_JWT_SIG_IS_EMPTY'] = False 218 | w3r = self.create_web3_request() 219 | for item in ['msg_query', 'auth']: 220 | if not self.request(w3r, item): 221 | self.passed = False 222 | return 223 | 224 | token = w3r.session.session_context['token'] 225 | tokens = token.split('.') 226 | if len(tokens) != 3: 227 | return 228 | 229 | fake_token = tokens[0]+'.'+tokens[1]+'.' 230 | if self.request(w3r, 'settings',{'token':fake_token}): 231 | self.results['NOT_CHECK_JWT_SIG_IS_EMPTY'] = True 232 | 233 | 234 | def safe_base64_decode(s): 235 | 236 | decoded_s = None 237 | try: 238 | decoded_s = base64.b64decode(s) 239 | except: 240 | try: 241 | decoded_s = base64.b64decode(s+'=') 242 | except: 243 | try: 244 | decoded_s = base64.b64decode(s+'==') 245 | except: 246 | return None 247 | 248 | if decoded_s == None: 249 | return None 250 | return decoded_s.decode('utf-8') -------------------------------------------------------------------------------- /web3_auth_checker/checkers/msg_security.py: -------------------------------------------------------------------------------- 1 | from .abstract_checker import AbstractChecker 2 | from .msg_tokenizer import TokenType, MsgTokenizer 3 | from ..web_request.web_session import sign_msg 4 | 5 | import time 6 | SLEEP_TIME = 60 7 | class MsgChecker(AbstractChecker): 8 | NAME = "msg_security" 9 | DESCRIPTION = "Check msg security" 10 | 11 | def _check(self): 12 | self.results = { 13 | # Design 14 | "URL_NOT_IN_MSG": True, 15 | "NAME_NOT_IN_MSG": True, 16 | "NONCE_NOT_IN_MSG": True, 17 | 18 | # Implementation 19 | "FAKE_MSG": None, # Fake msg + Nonce 20 | "REPLACE_URL": None, # Replace url 21 | "REPLACE_NAME": None, # Replace name 22 | "ADD_statement": None, # Add statement in msg 23 | 24 | "MSGS": [], 25 | } 26 | 27 | self.msgTokenizer = MsgTokenizer(self.web3.url, self.web3.name) 28 | 29 | self._request_msg() 30 | time.sleep(SLEEP_TIME) 31 | self._request_msg() 32 | # After 2 times request, we have 2 msg, and we can get nonce from msg 33 | t_labels = self.msgTokenizer.labels 34 | for t_label in t_labels: 35 | if t_label == TokenType.statement: 36 | continue 37 | if t_label == TokenType.domain: 38 | self.results['URL_NOT_IN_MSG'] = False 39 | elif t_label == TokenType.websiteName: 40 | self.results['NAME_NOT_IN_MSG'] = False 41 | elif t_label.value >= 400 and t_label.value < 500: 42 | self.results['NONCE_NOT_IN_MSG'] = False 43 | 44 | # Fake msg 45 | self._request_ge('FAKE_MSG', self.context_middleware_fake_msg) 46 | 47 | # Replace url 48 | if self.results['URL_NOT_IN_MSG'] == False: 49 | self._request_ge('REPLACE_URL', self.cm_replace_url) 50 | 51 | # Replace name 52 | if self.results['NAME_NOT_IN_MSG'] == False: 53 | self._request_ge('REPLACE_NAME', self.cm_replace_name) 54 | 55 | # Add statement 56 | self._request_ge('ADD_statement', self.cm_add_statement) 57 | 58 | # Nonce 59 | #self.nonces_request() 60 | 61 | self.logger.info(self.results) 62 | 63 | self.passed = True 64 | 65 | 66 | def _request_msg(self): 67 | w3r1 = self.create_web3_request() 68 | for item in ['msg_query', 'auth', 'settings']: 69 | time.sleep(3) 70 | if not self.request(w3r1, item): 71 | self.passed = False 72 | return False 73 | 74 | if "msg" in w3r1.session.session_context: 75 | msg = w3r1.session.session_context['msg'] 76 | self.msgTokenizer.add_msg(msg) 77 | self.results['MSGS'].append(('request_msg',msg)) 78 | 79 | 80 | def _request_ge(self, result, context_middleware): 81 | time.sleep(SLEEP_TIME) 82 | w3r = self.create_web3_request() 83 | for item in ['msg_query', 'auth', 'settings']: 84 | time.sleep(3) 85 | if not self.request(w3r, item, None, context_middleware): 86 | self.results[result] = False 87 | return 88 | self.results[result] = True 89 | 90 | def context_middleware_fake_msg(self,session_context): 91 | if "msg" not in session_context: 92 | return session_context 93 | 94 | self.results['MSGS'].append(('before_fake_msg',session_context['msg'])) 95 | 96 | self.msgTokenizer.add_msg(session_context['msg']) 97 | tokens = self.msgTokenizer.tokens 98 | labels = self.msgTokenizer.labels 99 | 100 | msg = "fake msg" 101 | for i, label in enumerate(labels): 102 | if label.value >= 400 and label.value < 500: 103 | msg += tokens[i] 104 | 105 | self.results['MSGS'].append(('fake_msg',msg)) 106 | 107 | session_context['msg'] = msg 108 | session_context['sig'] = sign_msg(msg, session_context['private_key']) 109 | 110 | return session_context 111 | 112 | def cm_replace_url(self, session_context): 113 | if "msg" not in session_context: 114 | return session_context 115 | 116 | self.msgTokenizer.add_msg(session_context['msg']) 117 | tokens = self.msgTokenizer.tokens 118 | labels = self.msgTokenizer.labels 119 | 120 | msg = "" 121 | for i, label in enumerate(labels): 122 | if label == TokenType.domain: 123 | msg += "http://fake.com"#tokens[i].replace(self.web3.url,"fake.com") 124 | else: 125 | msg += tokens[i] 126 | 127 | self.results['MSGS'].append(('replace_url',msg)) 128 | session_context['msg'] = msg 129 | session_context['sig'] = sign_msg(msg, session_context['private_key']) 130 | 131 | return session_context 132 | 133 | 134 | def cm_replace_name(self, session_context): 135 | if "msg" not in session_context: 136 | return session_context 137 | 138 | self.msgTokenizer.add_msg(session_context['msg']) 139 | tokens = self.msgTokenizer.tokens 140 | labels = self.msgTokenizer.labels 141 | 142 | msg = "" 143 | for i, label in enumerate(labels): 144 | if label == TokenType.websiteName: 145 | msg += "fake_name"#tokens[i].replace(self.web3.name,"fake") 146 | else: 147 | msg += tokens[i] 148 | 149 | self.results['MSGS'].append(('replace_name',msg)) 150 | session_context['msg'] = msg 151 | session_context['sig'] = sign_msg(msg, session_context['private_key']) 152 | 153 | return session_context 154 | 155 | def cm_add_statement(self, session_context): 156 | if "msg" not in session_context: 157 | return session_context 158 | 159 | msg = "something" + session_context['msg'] 160 | 161 | self.results['MSGS'].append(('add_statement',msg)) 162 | session_context['msg'] = msg 163 | session_context['sig'] = sign_msg(msg, session_context['private_key']) 164 | 165 | return session_context 166 | 167 | def nonces_request(self): 168 | if(self.results['NONCE'] != True): 169 | return 170 | 171 | tokens = self.msgTokenizer.tokens 172 | labels = self.msgTokenizer.labels 173 | for label, token in zip(labels, tokens): 174 | if label.value >= 400 and label.value < 500: 175 | self.results['NONCES']['NONCE_TYPE'] = label.name 176 | self.results['NONCES']['VALUE'] = token -------------------------------------------------------------------------------- /web3_auth_checker/checkers/msg_sig_security.py: -------------------------------------------------------------------------------- 1 | from .abstract_checker import AbstractChecker 2 | 3 | import time 4 | SLEEP_TIME = 60 5 | class MsgSigChecker(AbstractChecker): 6 | NAME = "msg_sig_security" 7 | DESCRIPTION = "Check msg signature security" 8 | 9 | def _check(self): 10 | self.results = { 11 | 'URL_NOT_IN_MSG': None, 12 | 'NAME_NOT_IN_MSG': None, 13 | "NONCE_NOT_IN_MSG": None, 14 | 15 | "NOT_CHECK_MSG": None, 16 | "NOT_CHECK_MSG_BODY": None, 17 | "NOT_CHECK_NONCE": None, 18 | "NOT_CHECK_SIG": None, 19 | 20 | "SIG_CAN_REPLAY": None, 21 | "SIG_FIRST_APPEAR": None, 22 | 23 | "MSG1": None, 24 | "MSG2": None, 25 | "NONCE_1": None, 26 | "NONCE_2": None, 27 | "MSG_BODY_1": None, 28 | "MSG_BODY_2": None, 29 | #'NONCE_TYPE':None, # Random or Timestamp 30 | "SIG_1": None, 31 | "SIG_2": None, 32 | 33 | #"CHECK_PADDING_HEAD": False, 34 | #"CHECK_PADDING_TAIL": False, 35 | 36 | "INFO":{ 37 | "NOT_CHECK_MSG":{ 38 | "MSG":None, 39 | "SIG":None, 40 | }, 41 | "NOT_CHECK_MSG_BODY":{ 42 | "MSG":None, 43 | "SIG":None, 44 | }, 45 | "NOT_CHECK_NONCE":{ 46 | "MSG":None, 47 | "SIG":None, 48 | }, 49 | "NOT_CHECK_SIG":{ 50 | "MSG":None, 51 | "SIG":None, 52 | }, 53 | } 54 | } 55 | 56 | self._msg_info() 57 | time.sleep(SLEEP_TIME) 58 | 59 | #1: Fake msg 60 | self._check_msg() 61 | time.sleep(SLEEP_TIME) 62 | 63 | self._check_msg_body() 64 | if self.results['NOT_CHECK_MSG_BODY'] is not None: # If not runned, skip the waiting 65 | time.sleep(SLEEP_TIME) 66 | 67 | self._check_nonce() 68 | if self.results['NOT_CHECK_NONCE'] is not None: 69 | time.sleep(SLEEP_TIME) 70 | self._check_sig() 71 | 72 | self.logger.info(self.results) 73 | 74 | self.passed = True 75 | 76 | def _msg_info(self): 77 | w3r1 = self.create_web3_request() 78 | for item in ['msg_query', 'auth', 'settings']: 79 | if not self.request(w3r1, item): 80 | self.passed = False 81 | return False 82 | 83 | if "msg" in w3r1.session.session_context: 84 | self.results['MSG1'] = w3r1.session.session_context['msg'] 85 | if "msg_body" in w3r1.session.session_context: 86 | self.results['MSG_BODY_1'] = w3r1.session.session_context['msg_body'] 87 | self.results['NOT_CHECK_MSG_BODY'] = True # For check_msg_body 88 | if "nonce" in w3r1.session.session_context: 89 | self.results['NONCE_1'] = w3r1.session.session_context['nonce'] 90 | self.results['NOT_CHECK_NONCE'] = True # For check_nonce 91 | 92 | if "sig" in w3r1.session.session_context: 93 | self.results['SIG_1'] = w3r1.session.session_context['sig'] 94 | 95 | time.sleep(SLEEP_TIME) 96 | w3r2 = self.create_web3_request() 97 | for item in ['msg_query', 'auth', 'settings']: 98 | if not self.request(w3r2, item): 99 | self.passed = False 100 | return False 101 | if self.results['SIG_FIRST_APPEAR'] is None and 'sig' in w3r2.session.session_context: 102 | self.results['SIG_FIRST_APPEAR'] = item 103 | 104 | 105 | if "msg" in w3r2.session.session_context: 106 | self.results['MSG2'] = w3r2.session.session_context['msg'] 107 | else: 108 | return 109 | if "msg_body" in w3r2.session.session_context: 110 | self.results['MSG_BODY_2'] = w3r2.session.session_context['msg_body'] 111 | if "nonce" in w3r2.session.session_context: 112 | self.results['NONCE_2'] = w3r2.session.session_context['nonce'] 113 | if "sig" in w3r2.session.session_context: 114 | self.results['SIG_2'] = w3r2.session.session_context['sig'] 115 | 116 | self.results['NAME_NOT_IN_MSG'] = True 117 | if self.web3.name.lower() in self.results['MSG1'].lower(): 118 | self.results['NAME_NOT_IN_MSG'] = False 119 | 120 | self.results['URL_NOT_IN_MSG'] = True 121 | if self.web3.url.lower() in self.results['MSG1'].lower(): 122 | self.results['URL_NOT_IN_MSG'] = False 123 | 124 | self.results['NONCE_NOT_IN_MSG'] = True 125 | if self.results['MSG1'] != self.results['MSG2']: 126 | self.results['NONCE_NOT_IN_MSG'] = False 127 | 128 | # SIG_CAN_REPLAY 129 | self.results['SIG_CAN_REPLAY'] = False 130 | item = self.results['SIG_FIRST_APPEAR'] 131 | if self.request_again(w3r2, item): 132 | self.results['SIG_CAN_REPLAY'] = True 133 | 134 | 135 | def _check_msg(self): 136 | self.results['NOT_CHECK_MSG'] = True 137 | w3r = self.create_web3_request() 138 | local_context = {'msg': 'fake_msg'} 139 | for item in ['msg_query', 'auth', 'settings']: 140 | if not self.request(w3r, item, local_context): 141 | self.results['NOT_CHECK_MSG'] = False 142 | break 143 | self.results['INFO']['NOT_CHECK_MSG']['MSG'] = w3r.session.session_context['msg'] 144 | self.results['INFO']['NOT_CHECK_MSG']['SIG'] = w3r.session.session_context['sig'] 145 | 146 | def _check_msg_body(self): 147 | # AFTER _msg_info 148 | 149 | if self.results['NOT_CHECK_MSG_BODY'] is None: 150 | return 151 | 152 | w3r = self.create_web3_request() 153 | local_context = {'msg_body': 'fake_body'} 154 | for item in ['msg_query', 'auth', 'settings']: 155 | if not self.request(w3r, item, local_context): 156 | self.results['NOT_CHECK_MSG_BODY'] = False 157 | break 158 | 159 | self.results['INFO']['NOT_CHECK_MSG_BODY']['MSG'] = w3r.session.session_context['msg'] 160 | self.results['INFO']['NOT_CHECK_MSG_BODY']['SIG'] = w3r.session.session_context['sig'] 161 | 162 | def _check_nonce(self): 163 | if self.results['NOT_CHECK_NONCE'] is None: 164 | return 165 | 166 | w3r = self.create_web3_request() 167 | local_context = {'nonce': 'fake_nonce'} 168 | for item in ['msg_query', 'auth', 'settings']: 169 | if not self.request(w3r, item, local_context): 170 | self.results['NOT_CHECK_NONCE'] = False 171 | break 172 | 173 | self.results['INFO']['NOT_CHECK_NONCE']['MSG'] = w3r.session.session_context['msg'] 174 | self.results['INFO']['NOT_CHECK_NONCE']['SIG'] = w3r.session.session_context['sig'] 175 | 176 | def _check_sig(self): 177 | self.results['NOT_CHECK_SIG'] = True 178 | w3r = self.create_web3_request() 179 | local_context = {'sig':'0x13d6babe50ad4056b7a768ed1dbe1b7a8fab35b3a598b324c202ccf3f7c5dbde78c633df080ffb17b7aea6bd0bc7901c280986c81fa406bd8d93faf5912d4d291c'} #'fake sig' account[0] 180 | for item in ['msg_query', 'auth', 'settings']: 181 | if not self.request(w3r, item, local_context): 182 | self.results['NOT_CHECK_SIG'] = False 183 | break 184 | 185 | self.results['INFO']['NOT_CHECK_SIG']['MSG'] = w3r.session.session_context['msg'] 186 | self.results['INFO']['NOT_CHECK_SIG']['SIG'] = w3r.session.session_context['sig'] 187 | 188 | ''' 189 | def _check_padding_head(self): 190 | w3r = self.create_web3_request() 191 | local_context = {'msg': 'fake_head $$ msg $$'} 192 | for item in ['msg_query', 'auth', 'settings']: 193 | if not self.request(w3r, item, local_context): 194 | self.results['CHECK_PADDING_HEAD'] = True 195 | return 196 | 197 | def _check_padding_tail(self): 198 | w3r = self.create_web3_request() 199 | local_context = {'msg': '$$ msg $$ fake_tail'} 200 | for item in ['msg_query', 'auth', 'settings']: 201 | if not self.request(w3r, item, local_context): 202 | self.results['CHECK_PADDING_TAIL'] = True 203 | return 204 | ''' -------------------------------------------------------------------------------- /web3_auth_checker/checkers/msg_tokenizer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | import enum 4 | 5 | class TokenType(enum.Enum): 6 | statement = 101 7 | domain = 201 8 | websiteName = 202 9 | address = 301 10 | 11 | nonce = 400 12 | rnd = 401 13 | timestamp10 = 402 14 | timestamp13 = 403 15 | datetime = 404 16 | 17 | 18 | class MsgTokenizer(): 19 | def __init__(self, _domain = None, _website_name =None, _step = 5): 20 | self.domain = _domain.lower() # lower case 21 | self.website_name = _website_name.lower() # lower case 22 | self.step = _step 23 | self.msgs = [] 24 | self.tokenss = [] 25 | self.labelss = [] 26 | 27 | def add_msg(self, msg): 28 | self._split_msg(msg) 29 | 30 | 31 | def _split_msg(self, msg): 32 | 33 | tokens = re.split(r'(https://|http://|/|\s+)', msg) #[ \f\n\r\t\v] 34 | 35 | # check tokenss length 36 | if len(self.tokenss) > 0: 37 | if len(self.tokenss[0]) != len(tokens): 38 | #raise Exception("The length of tokenss is not equal") 39 | return 40 | 41 | self.msgs.append(msg) 42 | 43 | labels = [] 44 | for token in tokens: 45 | if self.domain is not None and self.domain in token.lower(): 46 | labels.append(TokenType.domain) 47 | elif self.website_name is not None and self.website_name in token.lower(): 48 | labels.append(TokenType.websiteName) 49 | elif token.lower().startswith('0x'): 50 | labels.append(TokenType.address) 51 | else: 52 | labels.append(TokenType.statement) 53 | 54 | self.tokenss.append(tokens) 55 | self.labelss.append(labels) 56 | 57 | #Update nonce 58 | if len(self.tokenss) < 2: 59 | return 60 | 61 | step = self.step # Check 5 messages each time 62 | if len(self.tokenss) < step: 63 | step = len(self.tokenss) 64 | 65 | for i in range(len(self.tokenss[-1])): 66 | same_tokens = [] 67 | for j in range(0,step): 68 | same_tokens.append(self.tokenss[-1-j][i]) # get the ith token of jth tokens 69 | if len(set(same_tokens)) == 1: 70 | continue 71 | self.labelss[-1][i] = TokenType.nonce 72 | 73 | last_tokens = self.tokenss[-1] 74 | last_labels = self.labelss[-1] 75 | 76 | for i, label in enumerate(last_labels): 77 | if label != TokenType.nonce: 78 | continue 79 | 80 | token = last_tokens[i] 81 | if len(token) == 10: 82 | try: 83 | token = int(token) 84 | if token > 1225468800: # 2008-11-01 85 | last_labels[i] = TokenType.timestamp10 86 | continue 87 | except ValueError: 88 | pass 89 | 90 | if len(token) == 13: 91 | try: 92 | token = int(token) 93 | if token > 1225468800000: # 2008-11-01 94 | last_labels[i] = TokenType.timestamp13 95 | continue 96 | except ValueError: 97 | pass 98 | 99 | if len(token) == 24 or len(token) == 25 : #'2023-01-14T09:20:11.703Z' 100 | try: 101 | data = datetime.datetime.strptime(token[:23], '%Y-%m-%dT%H:%M:%S.%f') 102 | if data.year > 2008: 103 | last_labels[i] = TokenType.datetime 104 | continue 105 | except ValueError: 106 | pass 107 | 108 | last_labels[i] = TokenType.rnd 109 | 110 | #self.labelss[-1] = last_labels 111 | 112 | @property 113 | def msg(self): 114 | if len(self.msgs) == 0: 115 | return None 116 | return self.msgs[-1] 117 | 118 | @property 119 | def tokens(self): 120 | if len(self.tokenss) == 0: 121 | return None 122 | return self.tokenss[-1] 123 | 124 | @property 125 | def labels(self): 126 | if len(self.labelss) == 0: 127 | return None 128 | return self.labelss[-1] -------------------------------------------------------------------------------- /web3_auth_checker/checkers/nonce_security.py: -------------------------------------------------------------------------------- 1 | from .abstract_checker import AbstractChecker 2 | from .msg_tokenizer import TokenType, MsgTokenizer 3 | from ..web_request.web_session import sign_msg 4 | 5 | import time 6 | SLEEP_TIME = 61 7 | class NonceChecker(AbstractChecker): 8 | NAME = "nonce_security" 9 | DESCRIPTION = "Check nonce security" 10 | 11 | def _check(self): 12 | self.results = { 13 | # Design 14 | "NONCE_NOT_IN_MSG": True, 15 | "MSGS": [], 16 | } 17 | 18 | self.msgTokenizer = MsgTokenizer(self.web3.url, self.web3.name,4) 19 | 20 | # Nonce 21 | self.nonces_request(0) 22 | time.sleep(SLEEP_TIME) 23 | self.nonces_request(1) 24 | time.sleep(SLEEP_TIME) 25 | self.nonces_request(0) 26 | time.sleep(SLEEP_TIME) 27 | self.nonces_request(1) 28 | 29 | labels = self.msgTokenizer.labels 30 | for label in labels: 31 | if label.value >= 400 and label.value < 500: 32 | self.results['NONCE_NOT_IN_MSG'] = False 33 | break 34 | 35 | self.logger.info(self.results) 36 | 37 | self.passed = True 38 | 39 | 40 | def nonces_request(self, account_index = 0): 41 | w3r = self.create_web3_request(account_index) 42 | for item in ['msg_query', 'auth', 'settings']: 43 | if 'msg' in w3r.session.session_context: 44 | msg = w3r.session.session_context['msg'] 45 | self.results['MSGS'].append(msg) 46 | self.msgTokenizer.add_msg(msg) 47 | 48 | if not self.request(w3r, item): 49 | self.passed = False 50 | return False 51 | 52 | 53 | -------------------------------------------------------------------------------- /web3_auth_checker/checkers/request_items_checker.py: -------------------------------------------------------------------------------- 1 | from .abstract_checker import AbstractChecker 2 | from .msg_tokenizer import TokenType, MsgTokenizer 3 | 4 | class RequestItemsChecker(AbstractChecker): 5 | NAME = "request_items" 6 | DESCRIPTION = "Check all request items before checking the auth" 7 | 8 | def _check(self): 9 | self.results = { 10 | 'msg_query': None, 11 | 'auth': None, 12 | 'settings': None, 13 | 'logout': None, 14 | } 15 | w3r = self.create_web3_request() 16 | 17 | for item_name in self.REQUEST_ITEMS: 18 | r = self.request(w3r, item_name,None,self.context_middleware_msg) 19 | self.results[item_name] = r 20 | 21 | if not r: 22 | self.passed = False 23 | return 24 | 25 | self.passed = True 26 | 27 | 28 | def context_middleware_msg(self,session_context): 29 | msgTokenizer = MsgTokenizer(self.web3.url, self.web3.name) 30 | if "msg" in session_context: 31 | msgTokenizer.add_msg(session_context['msg']) 32 | msg = "" 33 | for token in msgTokenizer.tokens: 34 | msg = msg + token 35 | session_context['msg'] = msg 36 | 37 | return session_context 38 | -------------------------------------------------------------------------------- /web3_auth_checker/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import colorlog 3 | 4 | log_colors_config = { 5 | 'DEBUG': 'black', # cyan white 6 | 'INFO': 'black', 7 | 'WARNING': 'yellow', 8 | 'ERROR': 'red', 9 | 'CRITICAL': 'bold_red', 10 | } 11 | 12 | def logger_init(print_level=logging.INFO): 13 | # 1. Create a logger 14 | logger = logging.getLogger('web3_sig_auth') 15 | logger.setLevel(logging.DEBUG) 16 | 17 | 18 | # 2. Create a handler, used for writing log to file 19 | fh = logging.FileHandler('logs.log') 20 | fh.setLevel(logging.DEBUG) 21 | 22 | # 3. Create a handler, used for writing log to console 23 | ch = logging.StreamHandler() 24 | ch.setLevel(print_level) 25 | 26 | # 27 | #formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 28 | 29 | 30 | # 4. Create a formatter 31 | fh.setFormatter(logging.Formatter('[%(asctime)s] %(name)s line:%(lineno)d [%(levelname)s]: %(message)s')) 32 | ch.setFormatter( 33 | colorlog.ColoredFormatter( 34 | fmt='%(log_color)s[%(asctime)s] %(name)s line:%(lineno)d [%(levelname)s]: %(message)s', 35 | datefmt='%Y-%m-%d %H:%M:%S', 36 | log_colors=log_colors_config)) 37 | 38 | # 5. Add handler to logger 39 | logger.addHandler(fh) 40 | logger.addHandler(ch) 41 | 42 | return logger 43 | 44 | if __name__ == '__main__': 45 | logger = logger_init() 46 | logger.name = 'test' 47 | logger.debug('debug message') 48 | logger.info('info message') 49 | logger.warning('warn message') 50 | logger.error('error message') 51 | logger.critical('critical message') 52 | 53 | -------------------------------------------------------------------------------- /web3_auth_checker/web_request/README.md: -------------------------------------------------------------------------------- 1 | We set up a configuation file for each postman file. 2 | The configuration file is a JSON file that contains the following information: 3 | 4 | ```json 5 | { 6 | "schema": "1.0", 7 | "name":"", // The name of the web service 8 | "url":"", // The url of the web service 9 | "postman_file_name":"", // The postman file name 10 | "items":{ 11 | "msg_query":{ // request name 12 | "perform":"request", // perform type: request, skip 13 | "input":{ // These data will be loaded into the request 14 | "addr": "0x1234...", // this keyword will be fill in the msg. Keywords only fill once, so cannot recurse 15 | "msg":"Please sign this message\n Address:$$ addr $$ Nonce is $$ nonce $$", 16 | }, 17 | "output":{ // The output will be loaded into the next request 18 | /** 19 | json,path:{"kw":[]} 20 | state:200 21 | text 22 | html:html.text 23 | */ 24 | "output":{ 25 | "type":"json", 26 | "path":{ 27 | "nonce":["results","nonce"] // The nonce will be added to the session context 28 | } 29 | }, 30 | }, 31 | "update_request_args":{ 32 | "impersonate": "chrome110", // set the impersonate 33 | "timeout": 10, 34 | "params":{} // You can even update the params 35 | }, 36 | "perform_conf":{} //perform config 37 | }, 38 | 39 | 40 | "auth":{ 41 | "perform":"request", // request, input, skip 42 | "input":{ }, 43 | "output":{ 44 | "type":"json", 45 | "path":{ 46 | "token":["results","accessToken"] // The path of the message 47 | } 48 | }, 49 | "update_request_args":{ 50 | "impersonate": "chrome110", // set the impersonate 51 | }, 52 | "perform_conf":{ 53 | "sign_before_request":true 54 | } //perform config 55 | }, 56 | 57 | "settings":{ 58 | "perform":"request", // request, input, skip 59 | "input":{ }, 60 | "output":{ 61 | "type":"state", 62 | "code": 200 63 | }, 64 | "update_request_args":{ 65 | "impersonate": "chrome110", // set the impersonate 66 | }, 67 | }, 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /web3_auth_checker/web_request/__init__.py: -------------------------------------------------------------------------------- 1 | from .web_postman import WebServicePostman 2 | from .web_service import WebService, RequestItem 3 | from .web_session import WebSession,sign_msg -------------------------------------------------------------------------------- /web3_auth_checker/web_request/web_postman.py: -------------------------------------------------------------------------------- 1 | from .web_service import WebService, RequestItem 2 | import json 3 | import os 4 | 5 | class WebServicePostman(WebService): 6 | def __init__( 7 | self, 8 | ws_file_name:str, 9 | ws_dir:str = './web3_postman', 10 | postman_dir:str = 'postmans' 11 | ): 12 | ''' 13 | WebService.properties 14 | ''' 15 | self.ws_file_name = ws_file_name 16 | self.ws_dir = ws_dir 17 | super().__init__(os.path.join(ws_dir,ws_file_name)) 18 | 19 | ''' 20 | Postman.properties 21 | ''' 22 | self.postman_file_path:str =None 23 | self.postman_raw:json = None 24 | 25 | self.postman_file_path = os.path.join(ws_dir,postman_dir, self.webservice_raw['postman_file_name']) 26 | 27 | with open(self.postman_file_path,'r',encoding='utf-8') as f: 28 | self.postman_raw = json.load(f) 29 | 30 | if "https://schema.getpostman.com" not in self.postman_raw['info']['schema']: 31 | raise ImportError("Not a valid postman json file") 32 | 33 | self._make_request_items() 34 | 35 | 36 | def _make_request_items(self): 37 | ''' 38 | Combine web3.items and postman.items according name 39 | ''' 40 | postman_items = self._get_postman_request_items() 41 | 42 | for w_name, w_item in self.webservice_raw['items'].items(): 43 | if w_item == {}: 44 | self.request_items[w_name] = RequestItem(w_name) 45 | continue 46 | 47 | req_args = {} 48 | if w_name in postman_items: 49 | req_args = postman_items[w_name] 50 | 51 | # "impersonate" : "chrome110", 52 | #if 'impersonate' in w_item: req_args['impersonate'] = req_args['impersonate'] 53 | if 'update_request_args' in w_item: 54 | req_args.update(w_item['update_request_args']) 55 | 56 | sign = False 57 | if 'sign_before_request' in w_item: sign = w_item['sign_before_request'] 58 | 59 | self.request_items[w_name] = RequestItem( 60 | w_name, 61 | w_item['perform'], 62 | w_item['input'], 63 | w_item['output'], 64 | sign, 65 | req_args) 66 | 67 | def _get_postman_request_items(self): 68 | ''' 69 | postman.items as request arguments 70 | method: str, 71 | url: str, 72 | headers: Dict[str, str], 73 | params: Optional[Dict[str, str]] = {}, 74 | data = {}, 75 | #files: Optional[Dict] = {}, # Not implemented 76 | timeout:Optional[int]= 10 77 | ''' 78 | 79 | postman_items = {} 80 | 81 | for item in self.postman_raw['item']: 82 | if item == {} or 'request' not in item: 83 | continue 84 | 85 | item_request = item['request'] 86 | 87 | headers = {} 88 | for h in item_request['header']: 89 | headers[h['key']] = h['value'] 90 | #headers['User-Agent'] = 'PostmanRuntime/7.30.0' # add/replace User-Agent 91 | #'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.55' 92 | 93 | # Authorization 94 | if 'auth' in item['request']: 95 | auth = item['request']['auth'] 96 | if auth != {} and auth['type'] != 'noauth': 97 | if auth['type'] == 'bearer': 98 | headers['Authorization'] = 'Bearer $$ token $$' #+ auth['bearer'][0]['token'] 99 | 100 | # data: formdata 101 | data = _parse_body(item_request['body'],headers) 102 | # TODO: parse params 103 | params = {} 104 | 105 | postman_items[item['name']] = { 106 | 'method': item_request['method'], 107 | 'url': item_request['url']['raw'], 108 | 'headers': headers, 109 | 'params': params, 110 | 'data': data, 111 | 'timeout': 10, 112 | 'impersonate':None 113 | } 114 | 115 | return postman_items 116 | 117 | 118 | def _parse_body(body,headers): 119 | data = {} 120 | 121 | if body == {} or body is None or body['mode'] not in body: 122 | return {} 123 | 124 | if body['mode'] == 'raw': 125 | data = body['raw'] 126 | elif body['mode'] == 'formdata': 127 | data = {} 128 | for f in body['formdata']: 129 | data[f['key']] = f['value'] 130 | #data = MultipartFormData.format(data, headers=headers).encode('utf-8') 131 | data = MultipartFormData.format(data, headers=headers) 132 | elif body['mode'] == 'urlencoded': 133 | data = '' 134 | for f in body['urlencoded']: 135 | data += f['key'] + '=' + f['value'] + '&' 136 | data = data[:-1] 137 | 138 | return data 139 | 140 | 141 | class MultipartFormData(object): 142 | """ 143 | https://zhuanlan.zhihu.com/p/535027979 144 | multipart/form-data格式转化 145 | """ 146 | 147 | @staticmethod 148 | def format(data, boundary="----WebKitFormBoundaryaHKj8Ql1KX5XPkXc", headers=None) -> str: 149 | """ 150 | form data 151 | :param: data: {"req":{"cno":"18990876","flag":"Y"},"ts":1,"sig":1,"v": 2.0} 152 | :param: boundary: "----WebKitFormBoundary7MA4YWxkTrZu0gW" 153 | :param: headers: 包含boundary的头信息;如果boundary与headers同时存在以headers为准 154 | :return: str 155 | :rtype: str 156 | """ 157 | headers = headers or {} 158 | #从headers中提取boundary信息 159 | for key in headers.keys(): 160 | if key.lower() == "content-type": 161 | fd_val = str(headers[key]) 162 | if "boundary" in fd_val: 163 | fd_val = fd_val.split(";")[1].strip() 164 | boundary = fd_val.split("=")[1].strip() 165 | else: 166 | raise Exception("multipart/form-data error, content-type key does not have boundary") 167 | break 168 | #form-data格式定式 169 | jion_str = '--{}\r\nContent-Disposition: form-data; name="{}"\r\n\r\n{}\r\n' 170 | end_str = "--{}--".format(boundary) 171 | args_str = "" 172 | 173 | if not isinstance(data, dict): 174 | raise Exception("multipart/form-data parameters error") 175 | for key, value in data.items(): 176 | args_str = args_str + jion_str.format(boundary, key, value) 177 | 178 | args_str = args_str + end_str.format(boundary) 179 | #args_str = args_str.replace("\'", "\"") 180 | return args_str 181 | -------------------------------------------------------------------------------- /web3_auth_checker/web_request/web_service.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | import copy 3 | import json 4 | 5 | class RequestItem(): 6 | ''' 7 | A request item 8 | ''' 9 | def __init__( 10 | self, 11 | name:str, 12 | perform: str = 'skip' , # 'skip', 'request' 13 | input_payload: Dict = None, # web3.json 14 | output: Dict = None, # web3.json 15 | sign: Dict = None, # web3.json : "sign_before_request":true 16 | request_args: dict = None # postman request item 17 | ): 18 | 19 | self.name = name 20 | self.perform = perform 21 | self.input = input_payload or {} 22 | self.output = output or {} 23 | self.sign = sign or False 24 | self.request_args = request_args or {} 25 | ''' 26 | request_args: 27 | method, 28 | url, 29 | headers, 30 | params, 31 | data, 32 | timeout, 33 | impersonate, 34 | ''' 35 | self.response = None 36 | self.success = None 37 | self.local_context = {} 38 | self.session_context = {} 39 | 40 | def safe(self): 41 | return get_safe_request_item(self) 42 | 43 | 44 | def __str__(self) -> str: 45 | return f'name: {self.name}\nperform: {self.perform}\ninput: {self.input}\noutput: {self.output}\npsign: {self.sign}\nrequest_args: {self.request_args}\nresponse: {self.response}\nsuccess: {self.success}\nlocal_context: {self.local_context}\nsession_context: {self.session_context}\n' 46 | 47 | 48 | def _format_cookies(cookies): 49 | #TODO: expires 50 | f_cookies = {} 51 | for c_k in cookies: 52 | c_v = cookies.get(c_k) 53 | f_cookies[c_k] = c_v 54 | return f_cookies 55 | 56 | def get_safe_request_item(r:RequestItem): 57 | ''' 58 | Get a safe request item 59 | ''' 60 | sr = RequestItem(r.name) 61 | sr.perform = r.perform 62 | sr.input = copy.deepcopy(r.input) 63 | sr.output = copy.deepcopy(r.output) 64 | sr.sign = r.sign 65 | 66 | req_args = copy.deepcopy(r.request_args) 67 | if 'data' in req_args: 68 | req_args['data'] = req_args['data'].decode('utf-8') if isinstance(req_args['data'], bytes) else req_args['data'] 69 | sr.request_args = req_args 70 | 71 | sr.response = None if r.response is None else{ 72 | #'request': self.response.request.__dict__, 73 | 'url': r.response.url, 74 | 'content_text': r.response.text, 75 | 'status_code': r.response.status_code, 76 | 'reason': r.response.reason, 77 | 'ok': r.response.ok, 78 | #'headers': self.response.headers.__dict__, 79 | 'cookies': _format_cookies(r.response.cookies), 80 | 'elapsed': r.response.elapsed, 81 | 'encoding': r.response.encoding, 82 | 'charset': r.response.charset, 83 | 'redirect_count': r.response.redirect_count, 84 | 'redirect_url': r.response.redirect_url, 85 | }, 86 | 87 | sr.success = r.success 88 | sr.session_context = copy.deepcopy(r.session_context) 89 | sr.local_context = copy.deepcopy(r.local_context) 90 | 91 | return sr 92 | 93 | 94 | class WebService(): 95 | ''' 96 | A web service object, including a set of request items. 97 | ''' 98 | def __init__(self, ws_path:str): 99 | self.webservice_filename:str = ws_path.split('/')[-1].split('\\')[-1] 100 | self.webservice_path:str = ws_path 101 | self.webservice_raw = None 102 | self.name: str = None 103 | self.url: str = None 104 | self.request_items: dict = {} # name : RequestItem 105 | 106 | with open(self.webservice_path,'r',encoding='utf-8') as f: 107 | self.webservice_raw = json.load(f) 108 | self.name = self.webservice_raw['name'] 109 | self.url = self.webservice_raw['url'] 110 | self.auth_type = self.webservice_raw['auth_type'] # New property 111 | 112 | if self.webservice_raw['schema'] != '1.0': 113 | raise ImportError("Not a valid web3 file") 114 | 115 | def get_item(self, name:str): 116 | return self.request_items.get(name) 117 | -------------------------------------------------------------------------------- /web3_auth_checker/web_request/web_session.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Tuple, Union, cast 2 | from curl_cffi import requests 3 | from curl_cffi.requests import Session 4 | import time 5 | import json 6 | import copy 7 | import base64 8 | from .web_service import WebService 9 | 10 | class WebSession(): 11 | ''' 12 | The Websession manages requests and responses. 13 | ''' 14 | def __init__( 15 | self, 16 | session_context: dict = None, # global context 17 | session: Session = None, # requests.Session() 18 | ): 19 | 20 | self.session_context = session_context or {} 21 | self.session = session or requests.Session() 22 | 23 | self.before_request_items = [] 24 | self.after_request_items = [] 25 | 26 | def request( 27 | self, 28 | item: WebService, # RequestItem 29 | _local_context:dict=None, 30 | context_middleware = None 31 | ): 32 | ''' 33 | Perform a request 34 | 35 | Args: 36 | item (RequestItem): The request item 37 | local_context (dict, optional): The local context. Defaults to None. 38 | ''' 39 | if item == None: 40 | raise Exception('Request item is None') 41 | 42 | local_context = {} 43 | if _local_context: 44 | local_context = copy.deepcopy(_local_context) 45 | 46 | item.local_context = local_context 47 | self.before_request_items.append(item.safe()) 48 | 49 | ''' 50 | item.input -> overwrite -> session_context 51 | local_context -> overwrite -> input -> overwrite -> session_context 52 | ''' 53 | temp_context = copy.deepcopy(self.session_context) 54 | temp_context.update(item.input) # overwrite 55 | temp_context.update(local_context) # overwrite 56 | 57 | temp_context = _fill_data(temp_context, temp_context) # fill local_context, session_context, item.input 58 | temp_context = _fill_data(temp_context, temp_context) 59 | temp_context = _fill_data(temp_context, temp_context) # max support depth 3 60 | 61 | if "sig" not in local_context: 62 | if item.sign: 63 | temp_context['sig'] = sign_msg(temp_context['msg'], temp_context['private_key']) 64 | 65 | if context_middleware is not None: 66 | temp_context = context_middleware(temp_context) 67 | 68 | self.session_context = temp_context 69 | item.session_context = copy.deepcopy(self.session_context) 70 | item.output = _fill_data(item.output,temp_context) 71 | 72 | 73 | if item.perform == 'skip': 74 | return True 75 | 76 | if 'url' not in item.request_args: 77 | raise Exception(f'Request args incorrectly: {{item.request_args}}') 78 | 79 | if item.perform == 'request': 80 | request_args = _fill_data(item.request_args,temp_context) 81 | 82 | _check_fill_data(request_args) 83 | return self._request_perform(request_args, item) 84 | 85 | def request_again(self,item, local_context:dict=None,context_middleware = None): 86 | 87 | self.before_request_items.append(item.safe()) 88 | if item.perform == 'skip': return True 89 | if item.perform == 'request': return self._request_perform(item.request_args, item) 90 | return False 91 | 92 | def _request_perform(self, request_args, item): 93 | 94 | # curl_cffi : data must be dict, BytesIO or bytes 95 | if type(request_args['data']) == str: 96 | request_args['data'] = request_args['data'].encode('utf-8') 97 | 98 | #print('-----------------request_args-----------------\n',request_args) 99 | 100 | if "SLEEP" in item.input: 101 | time.sleep(item.input['SLEEP']) 102 | 103 | if 'NEW_SESSION' in item.input and item.input['NEW_SESSION'] == True: 104 | self.session = requests.Session() 105 | response = self.session.request(**request_args) 106 | 107 | # check output 108 | success = True 109 | if not response.ok: 110 | success = False 111 | else: 112 | text = response.text 113 | #print(text) 114 | if item.output['type'] == 'json': 115 | try: 116 | if "split_head" in item.output: 117 | text = text[item.output['split_head']:] 118 | if "split_tail" in item.output: 119 | text = text[:item.output['split_tail']] 120 | text = json.loads(text) 121 | success = _update_session_context_by_response(self.session_context,text,item.output['path']) 122 | except Exception as exception: 123 | #print(exception) 124 | success = False 125 | 126 | elif item.output['type'] == 'state': 127 | if item.output['code'] != response.status_code: 128 | success = False 129 | elif item.output['type'] == 'text': 130 | #if item.output['text'] != text: 131 | #success = False 132 | self.session_context['text'] = text 133 | elif item.output['type'] == 'html': 134 | if item.output['html'] not in text: 135 | success = False 136 | elif item.output['type'] == 'url': 137 | response.content = b"" # clear content 138 | if item.output['url'] != response.url: 139 | success = False 140 | 141 | item.request_args = request_args 142 | item.response = response 143 | item.success = success 144 | item.session_context = copy.deepcopy(self.session_context) 145 | 146 | self.after_request_items.append(item.safe()) 147 | return success 148 | 149 | 150 | 151 | from eth_account.messages import encode_defunct 152 | from eth_account import Account 153 | def sign_msg(msg, private_key, print_msg=False): 154 | ''' 155 | Sign message with private key 156 | ''' 157 | if print_msg: 158 | print('---------msg------------') 159 | print(repr(msg)) 160 | 161 | msg = encode_defunct(text=msg) 162 | account = Account.from_key(private_key) 163 | sig = account.sign_message(msg) 164 | 165 | if print_msg: 166 | print('---------sig------------') 167 | print(sig.signature.hex()) 168 | return sig.signature.hex() 169 | 170 | import re 171 | 172 | def _fill_data(d, context,print_msg=False): 173 | ''' 174 | Fill data by context 175 | 176 | input <-fill- session_context 177 | session_context <-update- input 178 | 179 | local_context <-fill- session_context 180 | session_context <-update- local_context 181 | 182 | request_args <-fill- session_context 183 | 184 | ''' 185 | if type(context) != dict: 186 | raise Exception('Invalid type: %s' % type(context)) 187 | 188 | text = '' 189 | if type(d) == dict: 190 | text = json.dumps(d) 191 | elif type(d) == str: 192 | text = d 193 | else: 194 | raise Exception('Invalid request args type: %s' % type(d)) 195 | 196 | kws = re.findall(r'\$\$.*?\$\$', text) 197 | for kw in kws: 198 | key = kw[2:-2].strip() 199 | if key.startswith('eval:'): # eval 200 | val = eval(key[5:]) 201 | val = str(val) 202 | text = text.replace(kw,val) 203 | else: 204 | if key in context: 205 | val = context[key] 206 | val = str(val) 207 | val = val.replace('"','\\\\\\\"').replace('\n','\\\\n').replace('\r','\\\\r') # only format the value 208 | text = text.replace(kw,val) 209 | else: 210 | continue 211 | #raise Exception('Key not found: %s' % key) 212 | if print_msg: 213 | print(f'kw:{kw}->val:{val}') 214 | return json.loads(text, strict=False) 215 | 216 | 217 | def _check_fill_data(d): 218 | ''' 219 | before request, check if all data is filled 220 | ''' 221 | if type(d) == dict: 222 | text = json.dumps(d) 223 | elif type(d) == str: 224 | text = d 225 | else: 226 | raise Exception('Invalid request args type: %s' % type(d)) 227 | 228 | kws = re.findall(r'\$\$.*?\$\$', text) 229 | if len(kws) > 0: 230 | print('-----------------request_args-----------------') 231 | print(text) 232 | raise Exception('Key not found:',kws[0][2:-2].strip() ) 233 | return True 234 | 235 | 236 | def _update_session_context_by_response(session_context,response,output_path): 237 | ''' 238 | update session_context with response 239 | ''' 240 | 241 | for key, paths in output_path.items(): 242 | t_rsp = response 243 | for path in paths: 244 | if path not in t_rsp: 245 | return False 246 | t_rsp = t_rsp[path] 247 | session_context[key] = t_rsp 248 | return True 249 | 250 | def _web3_auth(msg, private_key): 251 | sig = sign_msg(msg, private_key) 252 | web3 = {"signature":sig,"body":msg} 253 | print('---------web3------------') 254 | print(web3) 255 | web3_token = base64.b64encode(json.dumps(web3).encode('utf-8')).decode('utf-8') 256 | return web3_token 257 | 258 | def _msg_base64(msg): 259 | return base64.b64encode(msg.encode('utf-8')).decode('utf-8') --------------------------------------------------------------------------------