aboutsummaryrefslogblamecommitdiffstats
path: root/extract_token_from_browser.py
blob: 37549b4d9ffd0fb34ca09944119015c8bf2c4557 (plain) (tree)
1
2
3
4
5
6
7
8
                      



                                                                       


               


                    


              


                                                           
 











                                                



                            
                                                                
                                                
 
                        
                                         

 





                                                                        
                                                                                    
                    
                                                                                      
 

                          
                                                                            




                                                        


                                    

                                      
                                                                                   



                                                                               
                                       


                                                   

                                                          
         
                                                                                 
     
                                                                                        

               



                                               
                                                                                   

                       
                                                                                     




                               

                                                                     





                                                                               
                                                                                     


                                                       
                   
















                                                                         
    









                                                                       
 




                                                              
                                                         


                                          
                                                         
                                  







                                                                            

                                                  
 
                           
 
                                    
                                                                          

                                                         
                                                                            


                                                              


                                 
                                                                         
                                                                                    







                                                                             






                                                                    









                                                 





                                                                             

                                  
                                                                                 

                                                      


                                                  



                                                    
 
                     
                                                                                        

                                     
#!/usr/bin/env python3
# Examples:
# python ./extract_token_from_browser.py chrome --profile="Profile 2" \
#   --token=xoxc-foo-bar
# python ./extract_token_from_browser.py firefox-snap --token=xoxc-baz

import argparse
import json
import os
import secretstorage
import shutil
import sqlite3
import sys

from pathlib import Path
from secretstorage.exceptions import SecretStorageException
from sqlite3 import OperationalError

try:
    import_err = None
    from Crypto.Cipher import AES
    from Crypto.Protocol.KDF import PBKDF2
    from plyvel import DB
    from plyvel._plyvel import IOError as pIOErr
except ModuleNotFoundError as e:
    import_err = e
    pass


class AESCipher:
    def __init__(self, key):
        self.key = key

    def decrypt(self, text):
        cipher = AES.new(self.key, AES.MODE_CBC, IV=(b" " * 16))
        return self._unpad(cipher.decrypt(text))

    def _unpad(self, s):
        return s[: -ord(s[len(s) - 1 :])]


parser = argparse.ArgumentParser(
    description="Extract Slack tokens from the browser files"
)
parser.add_argument(
    "browser", help="Which browser to extract from", metavar="<browser>"
)
parser.add_argument("--token", help="Extracted token", metavar="<token>", nargs="?")
parser.add_argument(
    "--profile", help="Profile to look up cookies for", metavar="<profile>", nargs="?"
)
args = parser.parse_args()

if args.browser not in ["firefox", "firefox-snap", "chrome", "chrome-beta"]:
    print(
        "Currently only firefox. firefox-snap, chrome, "
        "chrome-beta are supported by this script",
        file=sys.stderr,
    )
    sys.exit(1)

if sys.platform.startswith("linux"):
    iterations = 1
    if args.browser == "firefox-snap":
        browser_data = Path.home().joinpath("snap/firefox/common/.mozilla/firefox")
    elif args.browser == "firefox":
        browser_data = Path.home().joinpath(".mozilla/firefox")
    else:
        browser_data = Path.home().joinpath(".config/google-%s" % args.browser)
elif sys.platform.startswith("darwin"):
    iterations = 1003
    if args.browser in ["firefox", "firefox-snap"]:
        browser_data = Path.home().joinpath(
            "Library/Application Support/Firefox/Profiles"
        )
    else:
        browser_data = Path.home().joinpath("Library/Application Support/Chrome")
else:
    print("Currently only Linux and macOS is supported by this script", file=sys.stderr)
    sys.exit(1)

leveldb_path = None
profile = args.profile
if args.browser in ["firefox", "firefox-snap"]:
    cookie_d_query = (
        "SELECT value FROM moz_cookies WHERE host = '.slack.com' " "AND name = 'd'"
    )
    cookie_ds_query = (
        "SELECT value FROM moz_cookies WHERE host = '.slack.com' " "AND name = 'd-s'"
    )
    if not profile:
        profile = "*.default*"

    default_profile_path = max(
        [x for x in browser_data.glob(profile)], key=os.path.getctime
    )
    if not default_profile_path:
        print("Couldn't find the default profile for Firefox", file=sys.stderr)
        sys.exit(1)
    cookies_path = default_profile_path.joinpath("cookies.sqlite")
else:
    if import_err:
        print("Missing required modules for Chrome browser support", file=sys.stderr)
        raise import_err

    # b'v10' is for Chromium, but not Chrome, it seems?
    prefix = b"v11"
    cookie_d_query = (
        "SELECT encrypted_value FROM cookies WHERE "
        "host_key = '.slack.com' AND name = 'd'"
    )
    cookie_ds_query = (
        "SELECT encrypted_value FROM cookies WHERE "
        "host_key = '.slack.com' AND name = 'd-s'"
    )
    if not profile:
        profile = "Default"

    default_profile_path = browser_data.joinpath(profile)
    cookies_path = default_profile_path.joinpath("Cookies")
    leveldb_path = default_profile_path.joinpath("Local Storage/leveldb")

cookie_d_value = None
cookie_ds_value = None
try:
    con = sqlite3.connect(f"file:{cookies_path}?immutable=1", uri=True)
    cookie_d_value = con.execute(cookie_d_query).fetchone()[0]
    cookie_ds_value = con.execute(cookie_ds_query).fetchone()[0]
except TypeError:
    if not cookie_d_value:
        print("Couldn't find the 'd' cookie value", file=sys.stderr)
        sys.exit(1)
    pass
finally:
    con.close()

if args.browser in ["chrome", "chrome-beta"]:
    bus = secretstorage.dbus_init()
    try:
        collection = secretstorage.get_default_collection(bus)
        for item in collection.get_all_items():
            if item.get_label() == "Chrome Safe Storage":
                passwd = item.get_secret()
                break
        else:
            raise Exception("Chrome password not found!")
    except SecretStorageException:
        print(
            "Error communicating org.freedesktop.secrets, trying 'peanuts' "
            "as a password",
            file=sys.stderr,
        )
        passwd = "peanuts"

    salt = b"saltysalt"
    length = 16
    key = PBKDF2(passwd, salt, length, iterations)

    cipher = AESCipher(key)

    if cookie_d_value[:3] == prefix:
        cookie_d_value = cipher.decrypt(cookie_d_value[3:]).decode("utf8")

    if cookie_ds_value and cookie_ds_value[:3] == prefix:
        cookie_ds_value = cipher.decrypt(cookie_ds_value[3:]).decode("utf8")

if cookie_ds_value:
    cookie_value = f"d={cookie_d_value};d-s={cookie_ds_value}"
else:
    cookie_value = cookie_d_value

local_storage_path = default_profile_path.joinpath("webappsstore.sqlite")
local_storage_query = "SELECT value FROM webappsstore2 WHERE key = 'localConfig_v2'"
teams = []
local_config = None
try:
    con = sqlite3.connect(f"file:{local_storage_path}?immutable=1", uri=True)
    local_config_str = con.execute(local_storage_query).fetchone()[0]
    local_config = json.loads(local_config_str)
except (OperationalError, TypeError):
    if not args.token and args.browser not in ["chrome", "chrome-beta"]:
        print(
            "Couldn't find the token for team, extract it manually "
            'by running \'window.prompt("Session token:", '
            "TS.boot_data.api_token)' in your browser dev console "
            "and specify it in --token",
            file=sys.stderr,
        )
        sys.exit(1)
finally:
    con.close()

if not local_config and leveldb_path:
    try:
        db = DB(str(leveldb_path))
    except pIOErr:
        leveldb_copy = str(leveldb_path) + ".bak"
        os.makedirs(leveldb_copy, exist_ok=True)
        shutil.copytree(leveldb_path, leveldb_copy, dirs_exist_ok=True)
        print(
            f"Leveldb was locked by a running browser - made an online copy "
            "of it in {leveldb_copy}",
            file=sys.stderr,
        )
        db = DB(str(leveldb_copy))

    local_storage_value = db.get(b"_https://app.slack.com\x00\x01localConfig_v2")
    local_config = json.loads(local_storage_value[1:])
    teams = [
        team
        for team in local_config["teams"].values()
        if not team["id"].startswith("E")
    ]

if not teams:
    teams = [{"token": args.token, "name": "Slack"}]

register_commands = [
    f"{team['name']}:\n/slack register {team['token']}:{cookie_value}" for team in teams
]
print("\n\n".join(register_commands))