├── requirements.txt ├── screenshots ├── 1pass.png ├── setup.png ├── results.png └── terminal.png ├── LICENSE ├── .gitignore ├── README.md └── gampass.py /requirements.txt: -------------------------------------------------------------------------------- 1 | unopass 2 | cryptography 3 | -------------------------------------------------------------------------------- /screenshots/1pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amadotejada/GAMpass/HEAD/screenshots/1pass.png -------------------------------------------------------------------------------- /screenshots/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amadotejada/GAMpass/HEAD/screenshots/setup.png -------------------------------------------------------------------------------- /screenshots/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amadotejada/GAMpass/HEAD/screenshots/results.png -------------------------------------------------------------------------------- /screenshots/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amadotejada/GAMpass/HEAD/screenshots/terminal.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Amado Tejada 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # macOS 10 | *.DS_Store 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 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 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GAMpass 2 | ##### Written by [Amado Tejada](https://www.linkedin.com/in/amadotejada/) 3 | 4 | ## 5 | Many Google Workspace admins use [GAM](https://github.com/GAM-team/GAM) or [GAMADV-XTD3](https://github.com/taers232c/GAMADV-XTD3) to manage their instance. Currently, the secrets needed for these tools are stored on disk in plaintext. 6 | 7 | *GAMpass* is a simple tool to encrypt & decrypt GAM secrets at runtime using your biometrics with [unopass](https://github.com/amadotejada/unopass) 8 | 9 | ## Requirements 10 | - python 3.13+ 11 | - `pip install -r requirements.txt` 12 | - [unopass](https://github.com/amadotejada/unopass) configured 13 | - GAM/GAMADV-XTD3 configured 14 | 15 | Tested on macOS 12.4+ 16 | 17 | ## First time setup 18 | **Back up your plaintext secrets in a different directory before you run setup.** 19 | **Once you verify you can encrypt/decrypt secrets you can delete the backup.** 20 | 21 | 1. Meet the requirements 22 | 2. Move `gampass.py` to the same directory as GAM's secrets files, usually `~/.gam/` 23 | 3. Run `python gampass.py setup`* 24 | - this encrypts GAM all secrets 25 | - ["client_secrets.json", "oauth2service.json", "oauth2.txt"] 26 | - If you have multiple GAM domains, all will be encrypted 27 | - this will generate a new `gampass.key` file. 28 | - this adds `gampass` and `gampass_cli` alias to ~/.zshrc 29 | - if you don't use ~/.zshrc, adjust in `gampass.py` 30 | 31 | 32 | 33 | 4. Open 1Password 34 | - create a vault named `gampass` 35 | - add a new password item with the title `gamkey` 36 | - add the content of the `gampass.key` the `credential` field 37 | 38 | 39 | 40 | ## GAM Usage 41 | Use this to make GAM calls 42 | 43 | Put `gampass` before the GAM command 44 | 45 | `gampass gam [gam args]` 46 | 47 | ```bash 48 | gampass gam select domain2 save | gam info domain 49 | ``` 50 | 51 | 52 | * macOS Touch ID prompts for your biometrics decrypting the secrets 53 | 54 | 55 | 56 | * GAM results 57 | 58 | ## GAMpass CLI Usage 59 | Use only this to manage your GAM secrets 60 | 61 | ```bash 62 | Usage: gampass_cli [option] 63 | 64 | Options: 65 | encrypt Encrypt GAM all secrets 66 | decrypt Decrypt GAM all secrets 67 | setup Setup a key and encrypt secrets 68 | updates View updates documentation 69 | sync Encrypt all domains with existing 1Password key 70 | Example: 71 | gampass_cli sync 72 | ``` 73 | 74 | ## Limitations 75 | Everything that works with GAM should work via *GAMpass*, except for the following: 76 | - Scheduled workflows via cron, etc., do not work because intentionally biometrics are prompted to decrypt the secrets. 77 | 78 | ## 79 | ### License 80 | 81 | *GAMpass* is released under the [MIT License](https://github.com/amadotejada/GAMpass/blob/main/LICENSE) 82 | 83 | #### 84 | -------------------------------------------------------------------------------- /gampass.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | 6 | from cryptography.fernet import Fernet 7 | from unopass import unopass as secret 8 | 9 | home = str(Path.home()) 10 | script_path = os.path.realpath(__file__) 11 | 12 | # A list of the GAM secrets that will be encrypted/decrypted. 13 | secrets = ["oauth2.txt", "oauth2service.json", "client_secrets.json"] 14 | 15 | 16 | def add_alias() -> None: 17 | """ 18 | Adds gampass and gampass_cli to user's `.zshrc` file 19 | If you don't use zsh, you can change the `.zshrc` file to your liking 20 | """ 21 | subprocess.Popen(r""" 22 | cat << EOF >> %s/.zshrc 23 | 24 | alias gampass='python %s decrypt && gampass_encrypt &!' 25 | alias gampass_cli='python %s $@' 26 | gampass_encrypt() 27 | { 28 | while true; do 29 | if [[ "$(pgrep -x gam)" ]]; then 30 | sleep 2 31 | else 32 | python %s encrypt 33 | break 34 | fi 35 | done 36 | } 37 | """ % (home, script_path, script_path, script_path), shell=True) 38 | 39 | 40 | def updates() -> None: 41 | print("\n* Go to this URL to update GAMADV-XTD3:") 42 | print("https://github.com/taers232c/GAMADV-XTD3/wiki/GamUpdates\n") 43 | 44 | 45 | def sync_key() -> None: 46 | return setup_key(key='existing_key') 47 | 48 | 49 | def setup_key(key=None) -> bool: 50 | """ 51 | Setup a new key, write it to the gam.key file, and encrypt the secrets 52 | Print instructions on how to add the key to 1Password 53 | :return: bool 54 | """ 55 | gam_path = os.path.dirname(os.path.realpath(__file__)) 56 | gam_key = Path(f"{gam_path}/gam.key") 57 | 58 | if not gam_key.is_file(): 59 | if not key: 60 | with open(f"{gam_path}/gam.key", "wb") as file: 61 | print("\n* Generating a new key...") 62 | key = Fernet.generate_key() 63 | file.write(str(key).encode()) 64 | key = key.decode() 65 | add_alias() 66 | else: 67 | print("\n* Using existing key...") 68 | key = secret.unopass("gampass", "gamkey", "password") 69 | 70 | if encrypt_file(key): 71 | print("* Encrypted GAM secrets") 72 | print(f"* Added gampass as alias to {home}/.zshrc\n") 73 | print("Add to your 1Password account as follows:") 74 | print("\t[1] Open 1Password") 75 | print("\t[2] Create a vault named gampass") 76 | print("\t[3] Add/Update a new password item with the title gamkey") 77 | print(f"\t[4] For 'password' add/update the {key}") 78 | print("\t[5] You're done!") 79 | print("\n* Note: The gam.key file will be deleted after a successful gampass run") 80 | print("\033[93m* Important: Create or Update the 1Password item with the key listed above\033[0m\n") 81 | return True 82 | else: 83 | Path(f"{gam_path}/gam.key").unlink(missing_ok=True) 84 | else: 85 | print(f"\n* GAMpass [gam.key] file already exists: {gam_path}/\n") 86 | return 87 | 88 | 89 | def encrypt_file(key=None) -> bool: 90 | """ 91 | Encrypts the secret files and renames it to `file.encrypted` 92 | :return: bool 93 | """ 94 | try: 95 | gam_path = os.path.dirname(os.path.realpath(__file__)) 96 | 97 | domains_secrets = [] 98 | for dirpath, dirnames, filenames in os.walk(gam_path): 99 | for file in filenames: 100 | if file in secrets: 101 | domains_secrets.append(f"{dirpath}/{file}") 102 | if domains_secrets: 103 | if not key: 104 | key = secret.unopass("gampass", "gamkey", "password") 105 | 106 | for domainfile in domains_secrets: 107 | if os.path.exists(domainfile): 108 | f = Fernet(key) 109 | secret.signout(deauthorize=True) 110 | with open(domainfile, "rb") as file: 111 | encrypted = file.read() 112 | encrypted = f.encrypt(encrypted) 113 | with open(f"{domainfile}.encrypted", "wb") as file: 114 | file.write(encrypted) 115 | Path(domainfile).unlink(missing_ok=True) 116 | 117 | if not domains_secrets: 118 | print("\n* No unencrypted secrets found in the GAM directory\n") 119 | secret.signout(deauthorize=True) 120 | return False 121 | else: 122 | return True 123 | 124 | except Exception as e: 125 | print(f"\n* Error: {e}") 126 | secret.signout(deauthorize=True) 127 | exit(1) 128 | 129 | 130 | def decrypt_file() -> None: 131 | """ 132 | Decrypts the encrypted secrets in the GAM directory, deletes encryption key 133 | :return: None 134 | """ 135 | try: 136 | print("\n* Decrypting GAM secrets via unopass:") 137 | # It's using the unopass library to decrypt the credential from 1Password. 138 | decrypt_key = secret.unopass("gampass", "gamkey", "password") 139 | gam_path = os.path.dirname(os.path.realpath(__file__)) 140 | domains_secrets = [] 141 | 142 | for dirpath, dirnames, filenames in os.walk(gam_path): 143 | for files in secrets: 144 | path = os.path.exists(f"{dirpath}/{files}.encrypted") 145 | if path: 146 | f = Fernet(decrypt_key) 147 | with open(f"{dirpath}/{files}.encrypted", "rb") as file: 148 | encrypted = file.read() 149 | decrypted = f.decrypt(encrypted) 150 | with open(f"{dirpath}/{files}", "wb") as file: 151 | file.write(decrypted) 152 | domains_secrets.append(f"{dirpath}/{files}") 153 | 154 | if domains_secrets: 155 | print(f"\033[93m* Decrypted {len(domains_secrets)//3} Google Workspace Domain!\033[0m") 156 | Path(f"{gam_path}/gam.key").unlink(missing_ok=True) 157 | print("\033[92m* Processing GAM request...\033[0m\n----------------------") 158 | else: 159 | print("\n* No encrypted secrets found in the GAM directory\n") 160 | secret.signout(deauthorize=True) 161 | exit(1) 162 | 163 | except Exception as e: 164 | print(f"\n\033[31m* Error: 1Pass key don't match!\033[0m\n---------------------------\n{e}") 165 | secret.signout(deauthorize=True) 166 | exit(1) 167 | 168 | 169 | def help_options() -> None: 170 | print("Use only this to manage your GAM secrets") 171 | print("\nUsage: gampass_cli [option]\n") 172 | print("Options:") 173 | print("\tencrypt \t\tEncrypt all GAM secrets") 174 | print("\tdecrypt \t\tDecrypt all GAM secrets") 175 | print("\tsetup \t\tSetup a key and encrypt secrets") 176 | print("\tupdates \t\tView updates documentation") 177 | print("\tsync \t\tEncrypt all domains with existing 1Password key") 178 | print("Example:") 179 | print("\tgampass_cli sync") 180 | print("\n") 181 | 182 | 183 | def main(): 184 | try: 185 | if sys.argv[1].lower() == "setup": 186 | setup_key() 187 | elif sys.argv[1].lower() == "encrypt": 188 | encrypt_file() 189 | elif sys.argv[1].lower() == "decrypt": 190 | decrypt_file() 191 | elif sys.argv[1].lower() == "sync": 192 | decrypt_file() 193 | sync_key() 194 | elif sys.argv[1].lower() == "update": 195 | updates() 196 | else: 197 | help_options() 198 | except IndexError: 199 | help_options() 200 | secret.signout(deauthorize=True) 201 | 202 | 203 | if __name__ == "__main__": 204 | main() 205 | --------------------------------------------------------------------------------