├── 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 |
--------------------------------------------------------------------------------