├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── LICENSE ├── README.md └── protosint.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pixelbubble 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 | # ProtOSINT 2 | ProtOSINT is a Python script that helps you investigate ProtonMail accounts and ProtonVPN IP addresses. 3 | 4 | ![](https://github.com/pixelbubble/pixelbubble/blob/main/protosint.gif) 5 | 6 | ## Description 7 | This tool can help you in your OSINT investigation on Proton service (for educational purposes only). 8 | ProtOSINT is separated in 3 sub-modules: 9 | - [1] Test the validity of one protonMail account and get additional information 10 | - [2] Try to find if your target have a protonMail account by generating multiple adresses by combining information fields inputted 11 | - [3] Find if your IP is currently affiliate to ProtonVPN 12 | 13 | ## :warning: Important update of the ProtonMail API [2021-11-07] :warning: 14 | Since several days, we observe an update in ProtonMail's API: 15 | - The API now seems to be limited to a few queries (ten/fifteen requests). 16 | - The blocking time is one hour (the limitation seems to be by IP address). 17 | - Even if an email is not valid, the API will return a result that seems valid with a random timestamp. 18 | - However, if the email is really valid, the timestamp returned is still good. 19 | 20 | **Advice for using ProtOSINT nowadays** 21 | - Use only module 1 and 3. 22 | - Before using module 1, first test the validity of your email with a third party tool or with the recipient field directly in the ProtonMail web interface: 23 | 24 | ![image](https://user-images.githubusercontent.com/75697623/140655959-e68ca0c7-3a3d-4cc0-8fdd-569792015e36.png) 25 | 26 | Then, using ProtOSINT, get additional information (the public key attached to the email, the date the PGP key was created and the encryption used). 27 | 28 | ## Prerequisite 29 | 30 | [Python 3](https://www.python.org/downloads/) 31 | 32 | ## Usage 33 | 34 | ```bash 35 | python3 protosint.py 36 | ``` 37 | 38 | ## Tips for ProtonMail investigation 39 | 40 | ### ProtonMail is case-insensitive 41 | 42 | The account name in the ProtonMail is case-insensitive and ProtonMail considers the "." "_" "-" symbols as transparent. 43 | Additionnaly, any words put after a "+" sign are not taken into account. 44 | It means that all of these email adresses below are the same as mikemike@protonmail.com: 45 | - "mike.mike@protonmail.com" 46 | - "mike_mike@protonmail.com" 47 | - "mike-mike@protonmail.com" 48 | - "mike.mike+paypal@protonmail.com" 49 | >All of these emails have the save timestamp and refers to the account mikemike@protonmail.com. 50 | 51 | ### Timestamp 52 | 53 | ProtOSINT does not always give you the creation time of the ProtonMail account itself. The timestamp returned by ProtonMail API is the time and date when the primary PGP key for the email was created. 54 | 55 | #### Example 1: my target keeps the default settings 56 | In this case, ProtOSINT gives me the real date of creation of the ProtonMail account. 57 | - 2021-01-12: Creation of the protonmail account "thisisatestemailaccount@protonmail.com" 58 | > Fingerprint of the key: 382a2045a09305f5ab4ef9000e1a2dd1e7e162fe - RSA (2048). 59 | - 2021-01-15: ProtOSINT gives me the 2021-01-12 timestamps 60 | 61 | #### Example 2: my target changes the email encryption keys 62 | In this case, ProtOSINT does not give me the "real" date of creation of the ProtonMail account but the date of creation of the primary PGP key. 63 | - 2021-01-12: Creation of the protonmail account "thisisatestemailaccount@protonmail.com" 64 | > Fingerprint of the key: 382a2045a09305f5ab4ef9000e1a2dd1e7e162fe - RSA (2048). 65 | - 2021-01-13: My target changes the primary PGP key (in settings/keys/Email encryption keys) 66 | > New fingerprint of the key: 634936a85115b8e30a31b94345d4551bc66da9d3 - RSA (2048). 67 | - 2021-01-15: ProtOSINT gives me the 2021-01-13 timestamp and not the other (2021-01-12) 68 | 69 | ### Email encryption keys 70 | 71 | ProtOSINT allow you to know which encryption key is used for a ProtonMail account: 72 | - RSA 2048-bit (Older but faster) - high security 73 | - RSA 4096-bit (Secure but slow) - highest security 74 | - X25519 (Modern, fastest, secure) - State-of-the-art 75 | 76 | ### Custom domain 77 | 78 | In the first sub-module of ProtOSINT [1], you can import a custom domain like alias@mycustumdomain.com. 79 | In fact, the premium ProtonMail plan allows you to connect your custom domain to ProtonMail. 80 | This means that if alias@mycustumdomain.com is "valid", your target uses a premium ProtonMail account. 81 | 82 | ## Contributing 83 | Feel free to clone this project. For major changes, please open an issue first to discuss what you would like to change. 84 | 85 | ## License 86 | [MIT](https://choosealicense.com/licenses/mit/) 87 | -------------------------------------------------------------------------------- /protosint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | #Python libraries 4 | import requests 5 | from datetime import datetime 6 | import re 7 | import ipaddress 8 | 9 | #Color setup 10 | class bcolors: 11 | OKGREEN = '\033[92m' 12 | WARNING = '\033[93m' 13 | FAIL = '\033[91m' 14 | BOLD = '\033[1m' 15 | ENDC = '\033[0m' 16 | 17 | 18 | def printAscii(): 19 | """ 20 | ASCII Art 21 | """ 22 | print(""" 23 | ___ _ _ _ 24 | / _ \_ __ ___ | |_ ___ ___(_)_ __ | |_ 25 | / /_)/ '__/ _ \| __/ _ \/ __| | '_ \| __| 26 | / ___/| | | (_) | || (_) \__ \ | | | | |_ 27 | \/ |_| \___/ \__\___/|___/_|_| |_|\__| 28 | 29 | """) 30 | 31 | 32 | def checkProtonAPIStatut(): 33 | """ 34 | This function check proton API statut : ONLINE / OFFLINE 35 | 36 | """ 37 | requestProton_mail_statut = requests.get('https://api.protonmail.ch/pks/lookup?op=index&search=test@protonmail.com') 38 | if requestProton_mail_statut.status_code == 200: 39 | print("Protonmail API is " + f"{bcolors.BOLD}ONLINE{bcolors.ENDC}") 40 | else: 41 | print("Protonmail API is " + f"{bcolors.BOLD}OFFLINE{bcolors.ENDC}") 42 | 43 | requestProton_vpn_statut = requests.get('https://api.protonmail.ch/vpn/logicals') 44 | if requestProton_vpn_statut.status_code == 200: 45 | print("Protonmail VPN is " + f"{bcolors.BOLD}ONLINE{bcolors.ENDC}") 46 | else: 47 | print("Protonmail VPN is " + f"{bcolors.BOLD}OFFLINE{bcolors.ENDC}") 48 | 49 | 50 | def printWelcome(): 51 | welcome = """ 52 | Let's take a look at your target: 53 | 1 - Test the validity of one protonmail account 54 | 2 - Try to find if your target have a protonmail account 55 | 3 - Find if your IP is currently affiliate to ProtonVPN 56 | """ 57 | print(welcome) 58 | 59 | 60 | def checkValidityOneAccount(): 61 | """ 62 | PROGRAM 1 : Test the validity of one protonmail account 63 | 64 | """ 65 | invalidEmail = True 66 | regexEmail = "([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)" 67 | 68 | print("You want to know if a protonmail email is real ?") 69 | while invalidEmail: 70 | #Input 71 | mail = input("Give me your email: ") 72 | #Text if the input is valid 73 | if(re.search(regexEmail,mail)): 74 | invalidEmail = False 75 | else: 76 | print("Invalid Email") 77 | invalidEmail = True 78 | 79 | #Check if the protonmail exist : valid / not valid 80 | requestProton = requests.get('https://api.protonmail.ch/pks/lookup?op=index&search='+str(mail)) 81 | bodyResponse = requestProton.text 82 | 83 | protonNoExist = "info:1:0" #not valid 84 | protonExist = "info:1:1" #valid 85 | 86 | if protonNoExist in bodyResponse: 87 | print("Protonmail email is " + f"{bcolors.FAIL}not valid{bcolors.ENDC}") 88 | 89 | if protonExist in bodyResponse: 90 | print("Protonmail email is " + f"{bcolors.OKGREEN}valid{bcolors.ENDC}") 91 | regexPattern1 = "2048:(.*)::" #RSA 2048-bit (Older but faster) 92 | regexPattern2 = "4096:(.*)::" #RSA 4096-bit (Secure but slow) 93 | regexPattern3 = "22::(.*)::" #X25519 (Modern, fastest, secure) 94 | try: 95 | timestamp = int(re.search(regexPattern1, bodyResponse).group(1)) 96 | dtObject = datetime.fromtimestamp(timestamp) 97 | print("Date and time of the creation:", dtObject) 98 | print("Encryption : RSA 2048-bit (Older but faster)") 99 | except: 100 | try: 101 | timestamp = int(re.search(regexPattern2, bodyResponse).group(1)) 102 | dtObject = datetime.fromtimestamp(timestamp) 103 | print("Date and time of the creation:", dtObject) 104 | print("Encryption : RSA 4096-bit (Secure but slow)") 105 | except: 106 | timestamp = int(re.search(regexPattern3, bodyResponse).group(1)) 107 | dtObject = datetime.fromtimestamp(timestamp) 108 | print("Date and time of the creation:", dtObject) 109 | print("Encryption : X25519 (Modern, fastest, secure)") 110 | 111 | #Download the public key attached to the email 112 | invalidResponse = True 113 | 114 | print("Do you want to download the public key attached to the email ?") 115 | while invalidResponse: 116 | #Input 117 | responseFromUser = input("""Please enter "yes" or "no": """) 118 | #Text if the input is valid 119 | if responseFromUser == "yes": 120 | invalidResponse = False 121 | requestProtonPublicKey = requests.get('https://api.protonmail.ch/pks/lookup?op=get&search='+str(mail)) 122 | bodyResponsePublicKey = requestProtonPublicKey.text 123 | print(bodyResponsePublicKey) 124 | elif responseFromUser == "no": 125 | invalidResponse = False 126 | else: 127 | print("Invalid Input") 128 | invalidResponse = True 129 | 130 | def checkGeneratedProtonAccounts(): 131 | """ 132 | PROGRAM 2 : Try to find if your target have a protonmail account by generating multiple adresses by combining information fields inputted 133 | 134 | """ 135 | 136 | #Input 137 | print("Let's go, try to find your protonmail target:") 138 | firstName = input("First name: ").lower() 139 | lastName = input("Last name: ").lower() 140 | yearOfBirth = input("Year of birth: ") 141 | pseudo1 = input("Pseudo 1: ").lower() 142 | pseudo2 = input("Pseudo 2: ").lower() 143 | zipCode = input("zipCode: ") 144 | 145 | #Protonmail domain 146 | domainList = ["@protonmail.com","@protonmail.ch","@pm.me"] 147 | 148 | #List of combinaison 149 | pseudoList=[] 150 | 151 | for domain in domainList: 152 | #For domain 153 | pseudoList.append(firstName+lastName+domain) 154 | pseudoList.append(lastName+firstName+domain) 155 | pseudoList.append(firstName[0]+lastName+domain) 156 | pseudoList.append(pseudo1+domain) 157 | pseudoList.append(pseudo2+domain) 158 | pseudoList.append(lastName+domain) 159 | pseudoList.append(firstName+lastName+yearOfBirth+domain) 160 | pseudoList.append(firstName[0]+lastName+yearOfBirth+domain) 161 | pseudoList.append(lastName+firstName+yearOfBirth+domain) 162 | pseudoList.append(pseudo1+yearOfBirth+domain) 163 | pseudoList.append(pseudo2+yearOfBirth+domain) 164 | pseudoList.append(firstName+lastName+yearOfBirth[-2:]+domain) 165 | pseudoList.append(firstName+lastName+yearOfBirth[-2:]+domain) 166 | pseudoList.append(firstName[0]+lastName+yearOfBirth[-2:]+domain) 167 | pseudoList.append(lastName+firstName+yearOfBirth[-2:]+domain) 168 | pseudoList.append(pseudo1+yearOfBirth[-2:]+domain) 169 | pseudoList.append(pseudo2+yearOfBirth[-2:]+domain) 170 | pseudoList.append(firstName+lastName+zipCode+domain) 171 | pseudoList.append(firstName[0]+lastName+zipCode+domain) 172 | pseudoList.append(lastName+firstName+zipCode+domain) 173 | pseudoList.append(pseudo1+zipCode+domain) 174 | pseudoList.append(pseudo2+zipCode+domain) 175 | pseudoList.append(firstName+lastName+zipCode[:2]+domain) 176 | pseudoList.append(firstName[0]+lastName+zipCode[:2]+domain) 177 | pseudoList.append(lastName+firstName+zipCode[:2]+domain) 178 | pseudoList.append(pseudo1+zipCode[:2]+domain) 179 | pseudoList.append(pseudo2+zipCode[:2]+domain) 180 | 181 | 182 | #Remove duplicates from list 183 | pseudoListUniq = [] 184 | for i in pseudoList: 185 | if i not in pseudoListUniq: 186 | pseudoListUniq.append(i) 187 | 188 | #Remove all irrelevant combinations 189 | for domain in domainList: 190 | if domain in pseudoListUniq: pseudoListUniq.remove(domain) 191 | if zipCode+domain in pseudoListUniq: pseudoListUniq.remove(zipCode+domain) 192 | if zipCode[:2]+domain in pseudoListUniq: pseudoListUniq.remove(zipCode[:2]+domain) 193 | if yearOfBirth+domain in pseudoListUniq: pseudoListUniq.remove(yearOfBirth+domain) 194 | if yearOfBirth[-2:]+domain in pseudoListUniq: pseudoListUniq.remove(yearOfBirth[-2:]+domain) 195 | if firstName+domain in pseudoListUniq: pseudoListUniq.remove(firstName+domain) 196 | 197 | print("===============================") 198 | print("I'm trying some combinaison: " + str(len(pseudoListUniq))) 199 | print("===============================") 200 | 201 | for pseudo in pseudoListUniq: 202 | requestProton = requests.get('https://api.protonmail.ch/pks/lookup?op=index&search='+str(pseudo)) 203 | bodyResponse = requestProton.text 204 | 205 | protonNoExist = "info:1:0" #not valid 206 | protonExist = "info:1:1" #valid 207 | 208 | if protonNoExist in bodyResponse: 209 | print(pseudo + " is " + f"{bcolors.FAIL}not valid{bcolors.ENDC}") 210 | 211 | if protonExist in bodyResponse: 212 | regexPattern1 = "2048:(.*)::" 213 | regexPattern2 = "4096:(.*)::" 214 | regexPattern3 = "22::(.*)::" 215 | try: 216 | timestamp = int(re.search(regexPattern1, bodyResponse).group(1)) 217 | dtObject = datetime.fromtimestamp(timestamp) 218 | print(pseudo + " is " + f"{bcolors.OKGREEN}valid{bcolors.ENDC}" + " - Creation date:", dtObject) 219 | except AttributeError: 220 | continue 221 | except: 222 | try: 223 | timestamp = int(re.search(regexPattern2, bodyResponse).group(1)) 224 | dtObject = datetime.fromtimestamp(timestamp) 225 | print(pseudo + " is " + f"{bcolors.OKGREEN}valid{bcolors.ENDC}" + " - Creation date:", dtObject) 226 | except AttributeError: 227 | continue 228 | except: 229 | timestamp = int(re.search(regexPattern3, bodyResponse).group(1)) 230 | dtObject = datetime.fromtimestamp(timestamp) 231 | print(pseudo + " is " + f"{bcolors.OKGREEN}valid{bcolors.ENDC}" + " - Creation date:", dtObject) 232 | 233 | def checkIPProtonVPN(): 234 | """ 235 | PROGRAM 3 : Find if your IP is currently affiliate to ProtonVPN 236 | 237 | """ 238 | while True: 239 | try: 240 | ip = ipaddress.ip_address(input('Enter IP address: ')) 241 | break 242 | except ValueError: 243 | continue 244 | 245 | requestProton_vpn = requests.get('https://api.protonmail.ch/vpn/logicals') 246 | bodyResponse = requestProton_vpn.text 247 | if str(ip) in bodyResponse: 248 | print("This IP is currently affiliate to ProtonVPN") 249 | else: 250 | print("This IP is currently not affiliate to ProtonVPN") 251 | #print(bodyResponse) 252 | 253 | 254 | # Entry point of the script 255 | def main(): 256 | printAscii() 257 | checkProtonAPIStatut() 258 | printWelcome() 259 | choice = input("Choose a program: ") 260 | if choice == "1": 261 | checkValidityOneAccount() 262 | if choice == "2": 263 | checkGeneratedProtonAccounts() 264 | if choice == "3": 265 | checkIPProtonVPN() 266 | 267 | if __name__ == '__main__': 268 | main() 269 | --------------------------------------------------------------------------------