├── .editorconfig
├── .github
├── CONTRIBUTING.md
├── dependabot.yml
└── workflows
│ ├── smokeshow.yml
│ └── test.yml
├── .gitignore
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── infisical
├── __init__.py
├── __version__.py
├── api
│ ├── __init__.py
│ ├── create_secret.py
│ ├── delete_secret.py
│ ├── get_secret.py
│ ├── get_secrets.py
│ ├── get_service_token_data.py
│ ├── get_service_token_data_key.py
│ ├── models.py
│ └── update_secret.py
├── client
│ ├── __init__.py
│ └── infisicalclient.py
├── constants.py
├── exceptions
│ └── __init__.py
├── helpers
│ ├── __init__.py
│ ├── client.py
│ └── secrets.py
├── logger.py
├── models
│ ├── __init__.py
│ ├── api.py
│ ├── models.py
│ └── secret_service.py
├── services
│ ├── __init__.py
│ └── secret_service.py
└── utils
│ ├── __init__.py
│ ├── crypto.py
│ └── http.py
├── pyproject.toml
├── scripts
├── format.sh
├── lint.sh
└── test.sh
└── tests
├── __init__.py
├── test_client
├── __init__.py
└── test_infisical_client.py
└── test_infisical.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | end_of_line = lf
10 | charset = utf-8
11 | indent_size = 4
12 |
13 | [*.py]
14 | indent_size = 4
15 |
16 | [*.{json,yml,toml}]
17 | indent_size = 2
18 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Start by cloning the repository:
2 | ```console
3 | $ git clone https://github.com/Infisical/infisical-python
4 | $ cd infisical-python
5 | ```
6 |
7 | We recommand that you create a virtual environment:
8 | ```console
9 | $ python -m venv env
10 | ```
11 |
12 | Then activate the environment with:
13 | ```console
14 | # For linux
15 | $ source ./env/bin/activate
16 |
17 | # For Windows PowerShell
18 | $ .\env\Scripts\Activate.ps1
19 | ```
20 |
21 | Make sure that you have the latest version of `pip` to avoid errors on the next step:
22 | ```console
23 | $ python -m pip install --upgrade pip
24 | ```
25 |
26 | Then install the project in editable mode and the dependencies with:
27 | ```console
28 | $ pip install -e '.[dev,test]'
29 | ```
30 |
31 | To run all the tests you can use the following command:
32 | ```console
33 | $ pytest tests
34 | ```
35 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # GitHub Actions
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 | commit-message:
9 | prefix: ⬆
10 | # Python
11 | - package-ecosystem: "pip"
12 | directory: "/"
13 | schedule:
14 | interval: "daily"
15 | commit-message:
16 | prefix: ⬆
17 |
--------------------------------------------------------------------------------
/.github/workflows/smokeshow.yml:
--------------------------------------------------------------------------------
1 | name: Smokeshow
2 |
3 | on:
4 | workflow_run:
5 | workflows: [Test]
6 | types: [completed]
7 |
8 | permissions:
9 | statuses: write
10 |
11 | jobs:
12 | smokeshow:
13 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/setup-python@v4
18 | with:
19 | python-version: "3.9"
20 |
21 | - run: pip install smokeshow
22 |
23 | - uses: dawidd6/action-download-artifact@v2.26.0
24 | with:
25 | workflow: test.yml
26 | commit: ${{ github.event.workflow_run.head_sha }}
27 |
28 | - run: smokeshow upload coverage-html
29 | env:
30 | SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage}
31 | # Threshold disable at the beginning, should be updated along with coverage progress
32 | SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 0
33 | SMOKESHOW_GITHUB_CONTEXT: coverage
34 | SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 | SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
36 | SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types: [opened, synchronize]
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
16 | fail-fast: false
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Set up Python
21 | uses: actions/setup-python@v4
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | cache: "pip"
25 | cache-dependency-path: pyproject.toml
26 | - uses: actions/cache@v3
27 | id: cache
28 | with:
29 | path: ${{ env.pythonLocation }}
30 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03
31 | - name: Install Dependencies
32 | if: steps.cache.outputs.cache-hit != 'true'
33 | run: pip install -e .[dev,test]
34 | - name: Lint
35 | run: bash scripts/lint.sh
36 | - run: mkdir coverage
37 | - name: Test
38 | run: bash scripts/test.sh
39 | env:
40 | COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
41 | CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
42 | - name: Store coverage files
43 | uses: actions/upload-artifact@v3
44 | with:
45 | name: coverage
46 | path: coverage
47 | coverage-combine:
48 | needs: [test]
49 | runs-on: ubuntu-latest
50 |
51 | steps:
52 | - uses: actions/checkout@v3
53 |
54 | - uses: actions/setup-python@v4
55 | with:
56 | python-version: "3.8"
57 | cache: "pip"
58 | cache-dependency-path: pyproject.toml
59 |
60 | - name: Get coverage files
61 | uses: actions/download-artifact@v3
62 | with:
63 | name: coverage
64 | path: coverage
65 |
66 | - run: pip install coverage[toml]
67 |
68 | - run: ls -la coverage
69 | - run: coverage combine coverage
70 | - run: coverage report
71 | - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}"
72 |
73 | - name: Store coverage HTML
74 | uses: actions/upload-artifact@v3
75 | with:
76 | name: coverage-html
77 | path: htmlcov
78 |
79 | # https://github.com/marketplace/actions/alls-green#why
80 | check: # This job does nothing and is only used for the branch protection
81 | if: always()
82 |
83 | needs:
84 | - coverage-combine
85 |
86 | runs-on: ubuntu-latest
87 |
88 | steps:
89 | - name: Decide whether the needed jobs succeeded or failed
90 | uses: re-actors/alls-green@release/v1
91 | with:
92 | jobs: ${{ toJSON(needs) }}
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,python,visualstudiocode
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,python,visualstudiocode
4 |
5 | ### Linux ###
6 | *~
7 |
8 | # temporary files which can be created if a process still has a handle open of a deleted file
9 | .fuse_hidden*
10 |
11 | # KDE directory preferences
12 | .directory
13 |
14 | # Linux trash folder which might appear on any partition or disk
15 | .Trash-*
16 |
17 | # .nfs files are created when an open file is removed but is still being accessed
18 | .nfs*
19 |
20 | ### Python ###
21 | # Byte-compiled / optimized / DLL files
22 | __pycache__/
23 | *.py[cod]
24 | *$py.class
25 |
26 | # C extensions
27 | *.so
28 |
29 | # Distribution / packaging
30 | .Python
31 | build/
32 | develop-eggs/
33 | dist/
34 | downloads/
35 | eggs/
36 | .eggs/
37 | lib/
38 | lib64/
39 | parts/
40 | sdist/
41 | var/
42 | wheels/
43 | share/python-wheels/
44 | *.egg-info/
45 | .installed.cfg
46 | *.egg
47 | MANIFEST
48 |
49 | # PyInstaller
50 | # Usually these files are written by a python script from a template
51 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
52 | *.manifest
53 | *.spec
54 |
55 | # Installer logs
56 | pip-log.txt
57 | pip-delete-this-directory.txt
58 |
59 | # Unit test / coverage reports
60 | htmlcov/
61 | .tox/
62 | .nox/
63 | .coverage
64 | .coverage.*
65 | .cache
66 | nosetests.xml
67 | coverage.xml
68 | *.cover
69 | *.py,cover
70 | .hypothesis/
71 | .pytest_cache/
72 | cover/
73 |
74 | # Translations
75 | *.mo
76 | *.pot
77 |
78 | # Django stuff:
79 | *.log
80 | local_settings.py
81 | db.sqlite3
82 | db.sqlite3-journal
83 |
84 | # Flask stuff:
85 | instance/
86 | .webassets-cache
87 |
88 | # Scrapy stuff:
89 | .scrapy
90 |
91 | # Sphinx documentation
92 | docs/_build/
93 |
94 | # PyBuilder
95 | .pybuilder/
96 | target/
97 |
98 | # Jupyter Notebook
99 | .ipynb_checkpoints
100 |
101 | # IPython
102 | profile_default/
103 | ipython_config.py
104 |
105 | # pyenv
106 | # For a library or package, you might want to ignore these files since the code is
107 | # intended to run in multiple environments; otherwise, check them in:
108 | # .python-version
109 |
110 | # pipenv
111 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
112 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
113 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
114 | # install all needed dependencies.
115 | #Pipfile.lock
116 |
117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
118 | __pypackages__/
119 |
120 | # Celery stuff
121 | celerybeat-schedule
122 | celerybeat.pid
123 |
124 | # SageMath parsed files
125 | *.sage.py
126 |
127 | # Environments
128 | .env
129 | .venv
130 | env/
131 | venv/
132 | ENV/
133 | env.bak/
134 | venv.bak/
135 |
136 | # Spyder project settings
137 | .spyderproject
138 | .spyproject
139 |
140 | # Rope project settings
141 | .ropeproject
142 |
143 | # mkdocs documentation
144 | /site
145 |
146 | # mypy
147 | .mypy_cache/
148 | .dmypy.json
149 | dmypy.json
150 |
151 | # Ruff
152 | .ruff_cache/
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | ### Windows ###
164 | # Windows thumbnail cache files
165 | Thumbs.db
166 | Thumbs.db:encryptable
167 | ehthumbs.db
168 | ehthumbs_vista.db
169 |
170 | # Dump file
171 | *.stackdump
172 |
173 | # Folder config file
174 | [Dd]esktop.ini
175 |
176 | # Recycle Bin used on file shares
177 | $RECYCLE.BIN/
178 |
179 | # Windows Installer files
180 | *.cab
181 | *.msi
182 | *.msix
183 | *.msm
184 | *.msp
185 |
186 | # Windows shortcuts
187 | *.lnk
188 |
189 | ### VisualStudioCode ###
190 | .vscode/*
191 | !.vscode/settings.json
192 | !.vscode/tasks.json
193 | !.vscode/launch.json
194 | !.vscode/extensions.json
195 | *.code-workspace
196 |
197 | # Local History for Visual Studio Code
198 | .history/
199 |
200 | ### VisualStudioCode Patch ###
201 | # Ignore all local history of files
202 | .history
203 | .ionide
204 |
205 | # End of https://www.toptal.com/developers/gitignore/api/windows,linux,python,visualstudiocode
206 |
207 | .infisical.json
208 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.linting.enabled": true,
3 | "python.formatting.provider": "none",
4 | "editor.formatOnSave": true,
5 | "python.linting.mypyEnabled": true,
6 | "[python]": {
7 | "editor.codeActionsOnSave": {
8 | "source.organizeImports": true
9 | },
10 | "editor.defaultFormatter": "ms-python.black-formatter"
11 | },
12 | "ruff.organizeImports": false,
13 | "ruff.fixAll": false,
14 | "ruff.importStrategy": "fromEnvironment",
15 | "autoDocstring.docstringFormat": "sphinx-notypes",
16 | "python.analysis.typeCheckingMode": "strict"
17 | }
18 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes will be documented in this file.
4 |
5 | ## [1.5.0] - 2023-10-01
6 |
7 | This version adds support for the Service Token V3 (Beta) authentication method for Infisical which is a JSON; note that it continues to support Service Token V2 (the default authentication method at this time). With this update, it's possible to initialize the InfisicalClient with a Service Token V3 JSON via the `tokenJSON` parameter to perform CRUD secret operations.
8 |
9 | Example:
10 |
11 | ```
12 | client = InfisicalClient(
13 | token_json=os.environ.get("INFISICAL_TOKEN_JSON")
14 | )
15 | ```
16 |
17 | ## [1.4.3] - 2023-09-13
18 |
19 | This version adds support for the `include_imports` and `attach_to_os_environ` parameters for the `get_all_secrets()` method.
20 |
21 | ## [1.4.2] - 2023-08-27
22 |
23 | This version patches the `path` parameter in `get_all_secrets()` so you can now fetch all secrets from a specific path.
24 |
25 |
26 | ## [1.4.1] - 2023-08-21
27 |
28 | This version updates returning unfound secrets to whatever is present on `os.environ` as opposed to returning `None`.
29 |
30 | ## [1.4.0] - 2023-07-13
31 |
32 | This version adds support for folders or path-based secret storage for all secret CRUD operations.
33 |
34 | ## [1.3.0] - 2023-05-05
35 |
36 | This version adds support for generating a symmetric encryption key, symmetric encryption, and decryption; algorithm used is `aes-256-gcm` with 96-bit `iv`.
37 |
38 | - `create_symmetric_key()`: Method to create a base64-encoded, 256-bit symmetric key.
39 | - `encrypt_symmetric()`: Method to symmetrically encrypt plaintext using the symmetric key.
40 | - `decrypt_symmetric()`: Method to symmetrically decrypt ciphertext using the symmetric key.
41 |
42 | To simplify things for developers, we stick to `base64` encoding and convert to and from bytes inside the methods.
43 |
44 | ## [1.2.0] - 2023-05-01
45 |
46 | Patched `expires_at` on `GetServiceTokenDetailsResponse` to be optional (to accomodate for cases where the service token never expires).
47 |
48 | ## [1.1.0] - 2023-04-27
49 |
50 | This version adds support for querying and mutating secrets by name with the introduction of blind-indexing. It also adds support for caching by passing in `cache_ttl`.
51 |
52 | - `get_all_secrets()`: Method to get all secrets from a project and environment
53 | - `create_secret()`: Method to create a secret
54 | - `get_secret()`: Method to get a secret by name
55 | - `update_secret()`: Method to update a secret by name
56 | - `delete_secret()`: Method to delete a secret by name
57 |
58 | The format of any fetched secrets from the SDK is now a `SecretBundle` that has useful properties like `secret_name`, `secret_value`, and `version`.
59 |
60 | This version also deprecates the `connect()` and `create_connection()` methods in favor of initializing the SDK with `new InfisicalClient(options)`
61 |
62 | It also includes some tests that can be run by passing in a `INFISICAL_TOKEN` and `SITE_URL` as environment variables to point the test client to an instance of Infisical.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Yohann MARTIN
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 |
6 |
7 |
Open-source, end-to-end encrypted tool to manage secrets and configs across your team and infrastructure.
8 |
9 |
10 | # Deprecated!
11 |
12 | This is now considered a legacy SDK, as we have released a new SDK that will be receiving all future updates. [You can find it here](https://pypi.org/project/infisical-python/).
13 |
14 | # Table of Contents
15 |
16 | - [Links](#links)
17 | - [Basic Usage](#basic-usage)
18 | - [Secrets](#working-with-secrets)
19 | - [Get Secrets](#get-secrets)
20 | - [Get Secret](#get-secret)
21 | - [Create Secret](#create-secret)
22 | - [Update Secret](#update-secret)
23 | - [Delete Secret](#delete-secret)
24 | - [Cryptography](#cryptography)
25 | - [Create Symmetric Key](#create-symmetric-key)
26 | - [Encrypt Symmetric](#encrypt-symmetric)
27 | - [Decrypt Symmetric](#decrypt-symmetric)
28 |
29 | # Links
30 |
31 | - [Infisical](https://github.com/Infisical/infisical)
32 |
33 | # Basic Usage
34 |
35 | ```py
36 | from flask import Flask
37 | from infisical import InfisicalClient
38 |
39 | app = Flask(__name__)
40 |
41 | client = InfisicalClient(token="your_infisical_token")
42 |
43 | @app.route("/")
44 | def hello_world():
45 | # access value
46 | name = client.get_secret("NAME", environment="dev", path="/")
47 | return f"Hello! My name is: {name.secret_value}"
48 | ```
49 |
50 | This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value.
51 |
52 | It is also possible to use the SDK to encrypt/decrypt text; the implementation uses `aes-256-gcm` with components of the encryption/decryption encoded in `base64`.
53 |
54 | ```python
55 | from infisical import InfisicalClient
56 |
57 | client = InfisicalClient()
58 |
59 | # some plaintext you want to encrypt
60 | plaintext = 'The quick brown fox jumps over the lazy dog'
61 |
62 | # create a base64-encoded, 256-bit symmetric key
63 | key = client.create_symmetric_key()
64 |
65 | # encrypt
66 | ciphertext, iv, tag = client.encrypt_symmetric(plaintext, key)
67 |
68 | # decrypt
69 | cleartext = client.decrypt_symmetric(ciphertext, key, iv, tag)
70 | ```
71 |
72 | # Installation
73 |
74 | You need Python 3.7+.
75 |
76 | ```console
77 | $ pip install infisical
78 | ```
79 |
80 | # Configuration
81 |
82 | Import the SDK and create a client instance with your [Infisical Token](https://infisical.com/docs/getting-started/dashboard/token).
83 |
84 | ```py
85 | from infisical import InfisicalClient
86 |
87 | client = InfisicalClient(token="your_infisical_token")
88 | ```
89 |
90 | Using Infisical Token V3 (Beta):
91 |
92 | In `v1.5.0`, we released a superior token authentication method; this credential is a JSON containing a `publicKey`, `privateKey`, and `serviceToken` and can be used to initialize the Node SDK client instead of the regular service token.
93 |
94 | You can use this beta feature like so:
95 |
96 | ```py
97 | from infisical import InfisicalClient
98 |
99 | client = InfisicalClient(token_json="your_infisical_token_v3_json")
100 | ```
101 |
102 | ### Options
103 |
104 | | Parameter | Type | Description |
105 | | ----------- | --------- | --------------------------------------------------------------------------- |
106 | | `token` | `string` | An Infisical Token scoped to a project and environment(s). |
107 | | `tokenJson` | `string` | An Infisical Token V3 JSON scoped to a project and environment(s) - in beta |
108 | | `site_url` | `string` | Your self-hosted Infisical site URL. Default: `https://app.infisical.com`. |
109 | | `cache_ttl` | `number` | Time-to-live (in seconds) for refreshing cached secrets. Default: `300`. |
110 | | `debug` | `boolean` | Turns debug mode on or off. Default: `false`. |
111 |
112 | ### Caching
113 |
114 | The SDK caches every secret and updates it periodically based on the provided `cache_ttl`. For example, if `cache_ttl` of `300` is provided, then a secret will be refetched 5 minutes after the first fetch; if the fetch fails, the cached secret is returned.
115 |
116 | # Secrets
117 |
118 | ## Get Secrets
119 |
120 | ```py
121 | secrets = client.get_all_secrets(environment="dev", path="/foo/bar/")
122 | ```
123 |
124 | Retrieve all secrets within a given environment and folder path. The service token used must have access to the given path and environment.
125 |
126 | ### Parameters
127 |
128 | - `environment` (string): The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
129 | - `path` (string): The path from where secrets should be fetched from.
130 | - `include_imports` (boolean): Whether or not to include imported secrets from the current path. Read about [secret import](https://infisical.com/docs/documentation/platform/secret-reference#import-entire-folders). If not specified, the default value is `True`.
131 | - `attach_to_os_environ` (boolean): Whether or not to attach fetched secrets to `os.environ`. If not specified, the default value is `False`.
132 |
133 | ## Get Secret
134 |
135 | ```py
136 | secret = client.get_secret("API_KEY", environment="dev", path="/")
137 | value = secret.secret_value # get its value
138 | ```
139 |
140 | By default, `get_secret()` fetches and returns a personal secret. If not found, it returns a shared secret, or tries to retrieve the value from `os.environ`. If a secret is fetched, `get_secret()` caches it to reduce excessive calls and re-fetches periodically based on the `cacheTTL` option (default is 300 seconds) when initializing the client — for more information, see the caching section.
141 |
142 | To explicitly retrieve a shared secret:
143 |
144 | ```py
145 | secret = client.get_secret(secret_name="API_KEY", type="shared", environment="dev", path="/")
146 | value = secret.secret_value # get its value
147 | ```
148 |
149 | ### Parameters
150 |
151 | - `secret_name` (string): The key of the secret to retrieve.
152 | - `environment` (string): The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
153 | - `path` (string): The path from where secrets should be fetched from.
154 | - `type` (string, optional): The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
155 |
156 | ## Create Secret
157 |
158 | Create a new secret in Infisical
159 |
160 | ```py
161 | new_api_key = client.create_secret("API_KEY", "FOO", environment="dev", path="/", type="shared")
162 | ```
163 |
164 | ### Parameters
165 |
166 | - `secret_name` (string): The key of the secret to create.
167 | - `secret_value` (string): The value of the secret.
168 | - `environment` (string): The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
169 | - `path` (string): The path from where secrets should be created.
170 | - `type` (string, optional): The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". A personal secret can only be created if a shared secret with the same name exists.
171 |
172 | ## Update Secret
173 |
174 | Update an existing secret in Infisical
175 |
176 | ```py
177 | updated_api_key = client.update_secret("API_KEY", "BAR", environment="dev", path="/", type="shared")
178 | ```
179 |
180 | ### Parameters
181 |
182 | - `secret_name` (string): The key of the secret to update.
183 | - `secret_value` (string): The new value of the secret.
184 | - `environment` (string): The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
185 | - `path` (string): The path from where secrets should be updated.
186 | - `type` (string, optional): The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
187 |
188 | ## Delete Secret
189 |
190 | Delete a secret in Infisical
191 |
192 | ```py
193 | deleted_secret = client.delete_secret("API_KEY", environment="dev", path="/", type="shared")
194 | ```
195 |
196 | ### Parameters
197 |
198 | - `secret_name` (string): The key of the secret to delete.
199 | - `environment` (string): The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
200 | - `path` (string): The path from where secrets should be deleted.
201 | - `type` (string, optional): The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
202 |
203 | # Cryptography
204 |
205 | ## Create Symmetric Key
206 |
207 | Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption.
208 |
209 | ```python
210 | key = client.create_symmetric_key()
211 | ```
212 |
213 | ### Returns
214 |
215 | `key` (string): A base64-encoded, 256-bit symmetric key.
216 |
217 | ## Encrypt Symmetric
218 |
219 | Encrypt plaintext -> ciphertext.
220 |
221 | ```python
222 | ciphertext, iv, tag = client.encrypt_symmetric(plaintext, key)
223 | ```
224 |
225 | ### Parameters
226 |
227 | - `plaintext` (string): The plaintext to encrypt.
228 | - `key` (string): The base64-encoded, 256-bit symmetric key to use to encrypt the `plaintext`.
229 |
230 | ### Returns
231 |
232 | - `ciphertext` (string): The base64-encoded, encrypted `plaintext`.
233 | - `iv` (string): The base64-encoded, 96-bit initialization vector generated for the encryption.
234 | - `tag` (string): The base64-encoded authentication tag generated during the encryption.
235 |
236 | ## Decrypt Symmetric
237 |
238 | Decrypt ciphertext -> plaintext/cleartext.
239 |
240 | ```python
241 | cleartext = client.decrypt_symmetric(ciphertext, key, iv, tag)
242 | ```
243 |
244 | ### Parameters
245 |
246 | - `ciphertext` (string): The ciphertext to decrypt.
247 | - `key` (string): The base64-encoded, 256-bit symmetric key to use to decrypt the `ciphertext`.
248 | - `iv` (string): The base64-encoded, 96-bit initiatlization vector generated for the encryption.
249 | - `tag` (string): The base64-encoded authentication tag generated during encryption.
250 |
251 | ### Returns
252 |
253 | `cleartext` (string): The decrypted encryption that is the cleartext/plaintext.
254 |
255 | # Contributing
256 |
257 | Bug fixes, docs, and library improvements are always welcome. Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/overview) for detailed information on how you can contribute.
258 |
259 | [//]: contributor-faces
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | ## Getting Started
268 |
269 | If you want to familiarize yourself with the SDK, you can start by [forking the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and [cloning it in your local development environment](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository).
270 |
271 | After cloning the repository, we recommend that you create a virtual environment:
272 |
273 | ```console
274 | $ python -m venv env
275 | ```
276 |
277 | Then activate the environment with:
278 |
279 | ```console
280 | # For linux
281 | $ source ./env/bin/activate
282 |
283 | # For Windows PowerShell
284 | $ .\env\Scripts\Activate.ps1
285 | ```
286 |
287 | Make sure that you have the latest version of `pip` to avoid errors on the next step:
288 |
289 | ```console
290 | $ python -m pip install --upgrade pip
291 | ```
292 |
293 | Then install the project in editable mode and the dependencies with:
294 |
295 | ```console
296 | $ pip install -e '.[dev,test]'
297 | ```
298 |
299 | To run existing tests, you need to make a `.env` at the root of this project containing a `INFISICAL_TOKEN` and `SITE_URL`. This will execute the tests against a project and environment scoped to the `INFISICAL_TOKEN` on a running instance of Infisical at the `SITE_URL` (this could be [Infisical Cloud](https://app.infisical.com)).
300 |
301 | To run all the tests you can use the following command:
302 |
303 | ```console
304 | $ pytest tests
305 | ```
306 |
307 | # License
308 |
309 | `infisical-python` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
310 |
--------------------------------------------------------------------------------
/infisical/__init__.py:
--------------------------------------------------------------------------------
1 | from .client.infisicalclient import InfisicalClient
2 |
3 | __all__ = ["InfisicalClient"]
4 |
--------------------------------------------------------------------------------
/infisical/__version__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.4.3"
2 |
--------------------------------------------------------------------------------
/infisical/api/__init__.py:
--------------------------------------------------------------------------------
1 | from infisical.__version__ import __version__
2 | from infisical.utils.http import BaseUrlSession, get_http_client
3 |
4 | USER_AGENT = f"InfisicalPythonSDK/{__version__}"
5 |
6 |
7 | def create_api_request_with_auth(base_url: str, service_token: str) -> BaseUrlSession:
8 | """Returns a :class:`requests.Session` with a ``base_url`` and the authorization
9 | bearer set to the ``service_token``.
10 |
11 | :param base_url: The base url to use
12 | :param service_token: The service token to use as a authorization bearer token
13 | :return: A :class:`requests.Session` instance preconfigured
14 | """
15 | api_request = get_http_client(base_url=base_url.rstrip("/"))
16 |
17 | api_request.headers.update({"User-Agent": USER_AGENT})
18 | api_request.headers.update({"Content-Type": "application/json"})
19 | api_request.headers.update({"Authorization": f"Bearer {service_token}"})
20 |
21 | return api_request
22 |
--------------------------------------------------------------------------------
/infisical/api/create_secret.py:
--------------------------------------------------------------------------------
1 | from infisical.models.api import CreateSecretDTO, SecretResponse
2 | from requests import Session
3 |
4 |
5 | def create_secret_req(api_request: Session, options: CreateSecretDTO) -> SecretResponse:
6 | response = api_request.post(
7 | url=f"/api/v3/secrets/{options.secret_name}",
8 | json={
9 | "workspaceId": options.workspace_id,
10 | "environment": options.environment,
11 | "type": options.type,
12 | "secretKeyCiphertext": options.secret_key_ciphertext,
13 | "secretKeyIV": options.secret_key_iv,
14 | "secretKeyTag": options.secret_key_tag,
15 | "secretValueCiphertext": options.secret_value_ciphertext,
16 | "secretValueIV": options.secret_value_iv,
17 | "secretValueTag": options.secret_value_tag,
18 | "secretPath": options.path,
19 | },
20 | )
21 |
22 | json_object = response.json()
23 |
24 | json_object["secret"]["workspace"] = options.workspace_id
25 | json_object["secret"]["environment"] = options.environment
26 |
27 |
28 |
29 | return SecretResponse.parse_obj(json_object)
30 |
--------------------------------------------------------------------------------
/infisical/api/delete_secret.py:
--------------------------------------------------------------------------------
1 | from infisical.models.api import DeleteSecretDTO, SecretResponse
2 | from requests import Session
3 |
4 |
5 | def delete_secret_req(api_request: Session, options: DeleteSecretDTO) -> SecretResponse:
6 | response = api_request.delete(
7 | url=f"/api/v3/secrets/{options.secret_name}",
8 | json={
9 | "workspaceId": options.workspace_id,
10 | "environment": options.environment,
11 | "type": options.type,
12 | "secretPath": options.path,
13 | },
14 | )
15 |
16 | json_object = response.json()
17 |
18 | json_object["secret"]["workspace"] = options.workspace_id
19 | json_object["secret"]["environment"] = options.environment
20 |
21 | return SecretResponse.parse_obj(json_object)
22 |
--------------------------------------------------------------------------------
/infisical/api/get_secret.py:
--------------------------------------------------------------------------------
1 | from infisical.models.api import GetSecretDTO, SecretResponse
2 | from requests import Session
3 |
4 |
5 | def get_secret_req(api_request: Session, options: GetSecretDTO) -> SecretResponse:
6 | response = api_request.get(
7 | url=f"/api/v3/secrets/{options.secret_name}",
8 | params={
9 | "workspaceId": options.workspace_id,
10 | "environment": options.environment,
11 | "type": options.type,
12 | "secretPath": options.path,
13 | },
14 | )
15 |
16 | json_object = response.json()
17 |
18 | json_object["secret"]["workspace"] = options.workspace_id
19 | json_object["secret"]["environment"] = options.environment
20 |
21 | return SecretResponse.parse_obj(json_object)
22 |
--------------------------------------------------------------------------------
/infisical/api/get_secrets.py:
--------------------------------------------------------------------------------
1 | from infisical.models.api import GetSecretsDTO, SecretsResponse
2 | from requests import Session
3 |
4 |
5 | def get_secrets_req(api_request: Session, options: GetSecretsDTO) -> SecretsResponse:
6 | """Send request again Infisical API to fetch secrets.
7 | See more information on https://infisical.com/docs/api-reference/endpoints/secrets/read
8 |
9 | :param api_request: The :class:`requests.Session` instance used to perform the request
10 | :param workspace_id: The ID of the workspace
11 | :param environment: The environment
12 | :return: Returns the API response as-is
13 | """
14 |
15 | response = api_request.get(
16 | "/api/v3/secrets",
17 | params={
18 | "environment": options.environment,
19 | "workspaceId": options.workspace_id,
20 | "secretPath": options.path,
21 | "include_imports": str(options.include_imports).lower()
22 | },
23 | )
24 |
25 | json_object = response.json()
26 |
27 | for obj in json_object['secrets']:
28 | obj["workspace"] = options.workspace_id
29 | obj["environment"] = options.environment
30 |
31 | for obj in json_object['imports']:
32 | for obj in obj['secrets']:
33 | obj["workspace"] = options.workspace_id
34 | obj["environment"] = options.environment
35 |
36 | data = SecretsResponse.parse_obj(json_object)
37 |
38 | return (data.secrets if data.secrets else [], data.imports if data.imports else [])
39 |
40 |
--------------------------------------------------------------------------------
/infisical/api/get_service_token_data.py:
--------------------------------------------------------------------------------
1 | from infisical.api.models import GetServiceTokenDetailsResponse
2 | from requests import Session
3 |
4 |
5 | def get_service_token_data_req(
6 | api_request: Session,
7 | ) -> GetServiceTokenDetailsResponse:
8 | """Send request again Infisical API to fetch service token data.
9 | See more information on https://infisical.com/docs/api-reference/endpoints/service-tokens/get
10 |
11 | :param api_request: The :class:`requests.Session` instance used to perform the request
12 | :return: Returns the API response as-is
13 | """
14 | response = api_request.get("/api/v2/service-token")
15 |
16 | return GetServiceTokenDetailsResponse.parse_obj(response.json())
17 |
--------------------------------------------------------------------------------
/infisical/api/get_service_token_data_key.py:
--------------------------------------------------------------------------------
1 | from infisical.api.models import GetServiceTokenKeyResponse
2 | from requests import Session
3 |
4 |
5 | def get_service_token_data_key_req(
6 | api_request: Session,
7 | ) -> GetServiceTokenKeyResponse:
8 | """Send request again Infisical API to fetch service token data v3 key.
9 |
10 | :param api_request: The :class:`requests.Session` instance used to perform the request
11 | :return: Returns the API response as-is
12 | """
13 | response = api_request.get("/api/v3/service-token/me/key")
14 |
15 | return GetServiceTokenKeyResponse.parse_obj(response.json())
16 |
--------------------------------------------------------------------------------
/infisical/api/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel, Field
5 |
6 |
7 | class GetServiceTokenDetailsResponse(BaseModel):
8 | id: str = Field(..., alias="id")
9 | name: str
10 | workspace: str = Field(..., alias="projectId")
11 | expires_at: Optional[datetime] = Field(None, alias="expiresAt")
12 | encrypted_key: str = Field(..., alias="encryptedKey")
13 | iv: str
14 | tag: str
15 | created_at: datetime = Field(..., alias="createdAt")
16 | updated_at: datetime = Field(..., alias="updatedAt")
17 | #v: int = Field(..., alias="__v")
18 |
19 | class KeyData(BaseModel):
20 | id: str = Field(..., alias="id")
21 | workspace: str
22 | encrypted_key: str = Field(..., alias="encryptedKey")
23 | public_key: str = Field(..., alias="publicKey")
24 | nonce: str
25 |
26 | class GetServiceTokenKeyResponse(BaseModel):
27 | key: KeyData
--------------------------------------------------------------------------------
/infisical/api/update_secret.py:
--------------------------------------------------------------------------------
1 | from infisical.models.api import SecretResponse, UpdateSecretDTO
2 | from requests import Session
3 |
4 |
5 | def update_secret_req(api_request: Session, options: UpdateSecretDTO) -> SecretResponse:
6 | response = api_request.patch(
7 | url=f"/api/v3/secrets/{options.secret_name}",
8 | json={
9 | "workspaceId": options.workspace_id,
10 | "environment": options.environment,
11 | "type": options.type,
12 | "secretValueCiphertext": options.secret_value_ciphertext,
13 | "secretValueIV": options.secret_value_iv,
14 | "secretValueTag": options.secret_value_tag,
15 | "secretPath": options.path,
16 | },
17 | )
18 |
19 | json_object = response.json()
20 |
21 | json_object["secret"]["workspace"] = options.workspace_id
22 | json_object["secret"]["environment"] = options.environment
23 |
24 | return SecretResponse.parse_obj(json_object)
25 |
--------------------------------------------------------------------------------
/infisical/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Infisical/infisical-python/28bf675217ae8713b5480974b195e7a62847fe3d/infisical/client/__init__.py
--------------------------------------------------------------------------------
/infisical/client/infisicalclient.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Dict, Optional
3 |
4 | from infisical.api import create_api_request_with_auth
5 | from infisical.constants import (
6 | AUTH_MODE_SERVICE_TOKEN,
7 | AUTH_MODE_SERVICE_TOKEN_V3,
8 | INFISICAL_URL,
9 | SERVICE_TOKEN_REGEX,
10 | )
11 | from infisical.exceptions import InfisicalTokenError
12 | from infisical.helpers.client import (
13 | create_secret_helper,
14 | delete_secret_helper,
15 | get_all_secrets_helper,
16 | get_secret_helper,
17 | update_secret_helper,
18 | )
19 | from infisical.models.models import SecretBundle
20 | from infisical.models.secret_service import ClientConfig
21 | from infisical.utils.crypto import (
22 | create_symmetric_key_helper,
23 | decrypt_symmetric_helper,
24 | encrypt_symmetric_helper,
25 | )
26 | from typing_extensions import Literal
27 |
28 |
29 | class InfisicalClient:
30 | def __init__(
31 | self,
32 | token: Optional[str] = None,
33 | token_json: Optional[str] = None,
34 | site_url: str = INFISICAL_URL,
35 | debug: bool = False,
36 | cache_ttl: int = 300,
37 | ):
38 | self.cache: Dict[str, SecretBundle] = {}
39 | self.client_config: Optional[ClientConfig] = None
40 |
41 | if token and token != "":
42 | token_match = SERVICE_TOKEN_REGEX.fullmatch(token)
43 |
44 | if token_match is None:
45 | raise InfisicalTokenError("The token is not in correct format!")
46 |
47 | service_token = token_match.group(1)
48 | service_token_key = token_match.group(2)
49 |
50 | self.client_config = ClientConfig(
51 | auth_mode=AUTH_MODE_SERVICE_TOKEN,
52 | credentials={"service_token_key": service_token_key},
53 | cache_ttl=cache_ttl,
54 | )
55 |
56 | self.api_request = create_api_request_with_auth(site_url, service_token)
57 |
58 | if token_json and token_json != "":
59 | token_dict = json.loads(token_json)
60 |
61 | self.client_config = ClientConfig(
62 | auth_mode=AUTH_MODE_SERVICE_TOKEN_V3,
63 | credentials={
64 | "public_key": token_dict["publicKey"],
65 | "private_key": token_dict["privateKey"]
66 | },
67 | cache_ttl=cache_ttl
68 | )
69 |
70 | self.api_request = create_api_request_with_auth(site_url, token_dict["serviceToken"])
71 |
72 | self.debug = debug
73 |
74 | print("WARNING: You are using a deprecated version of the Infisical SDK. Please use the new Infisical SDK found here: https://pypi.org/project/infisical-python/")
75 |
76 | def get_all_secrets(
77 | self,
78 | environment: str = "dev",
79 | path: str = "/",
80 | include_imports: bool = True,
81 | attach_to_os_environ: bool = False
82 | ):
83 | """Return all the secrets accessible by the instance of Infisical"""
84 | return get_all_secrets_helper(self, environment, path, include_imports, attach_to_os_environ)
85 |
86 | def get_secret(
87 | self,
88 | secret_name: str,
89 | type: Literal["shared", "personal"] = "personal",
90 | environment: str = "dev",
91 | path: str = "/",
92 | ) -> SecretBundle:
93 | """Return secret with name `secret_name`
94 |
95 | :param secret_name: Key of secret
96 | :param type: Type of secret that is either "shared" or "personal"
97 | :return: Secret bundle for secret with name `secret_name`
98 | """
99 | return get_secret_helper(self, secret_name, type, environment, path)
100 |
101 | def create_secret(
102 | self,
103 | secret_name: str,
104 | secret_value: str,
105 | type: Literal["shared", "personal"] = "shared",
106 | environment: str = "dev",
107 | path: str = "/",
108 | ) -> SecretBundle:
109 | """Create secret with name `secret_name` and value `secret_value`
110 |
111 | :param secret_name: Name of secret to create
112 | :param secret_value: Value of secret to create
113 | :param type: Type of secret to create that is either "shared" or "personal"
114 | :return: Secret bundle for created secret with name `secret_name`
115 | """
116 | return create_secret_helper(
117 | self, secret_name, secret_value, type, environment, path
118 | )
119 |
120 | def update_secret(
121 | self,
122 | secret_name: str,
123 | secret_value: str,
124 | type: Literal["shared", "personal"] = "shared",
125 | environment: str = "dev",
126 | path: str = "/",
127 | ) -> SecretBundle:
128 | """Update secret with name `secret_name` and value `secret_value`
129 |
130 | :param secret_name: Name of secret to update
131 | :param secret_value: New value of secret to update
132 | :param type: Type of secret to update that is either "shared" or "personal"
133 | :return: Secret bundle for updated secret with name `secret_name`
134 | """
135 | return update_secret_helper(
136 | self, secret_name, secret_value, type, environment, path
137 | )
138 |
139 | def delete_secret(
140 | self,
141 | secret_name: str,
142 | type: Literal["shared", "personal"] = "shared",
143 | environment: str = "dev",
144 | path: str = "/",
145 | ):
146 | """Delete secret with name `secret_name`
147 |
148 | :param secret_name: Name of secret to delete
149 | :param type: Type of secret to update that is either "shared" or "personal"
150 | :return: Secret bundle for updated secret with name `secret_name`
151 | """
152 | return delete_secret_helper(self, secret_name, type, environment, path)
153 |
154 | def create_symmetric_key(self) -> str:
155 | """Create a base64-encoded, 256-bit symmetric key"""
156 | return create_symmetric_key_helper()
157 |
158 | def encrypt_symmetric(self, plaintext: str, key: str):
159 | """Encrypt the plaintext `plaintext` with the (base64) 256-bit secret key `key`"""
160 | return encrypt_symmetric_helper(plaintext, key)
161 |
162 | def decrypt_symmetric(self, ciphertext: str, key: str, iv: str, tag: str):
163 | """Decrypt the ciphertext `ciphertext` with the (base64) 256-bit secret key `key`, provided `iv` and `tag`"""
164 | return decrypt_symmetric_helper(ciphertext, key, iv, tag)
165 |
--------------------------------------------------------------------------------
/infisical/constants.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | INFISICAL_URL = "https://app.infisical.com"
4 | AUTH_MODE_SERVICE_TOKEN = "service_token"
5 | AUTH_MODE_SERVICE_TOKEN_V3 = "service_token_v3"
6 |
7 | SERVICE_TOKEN_REGEX = re.compile(r"(st\.[a-f0-9-]+\.[a-f0-9]+)\.([a-f0-9]+)")
8 |
--------------------------------------------------------------------------------
/infisical/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | class InfisicalTokenError(ValueError):
2 | pass
3 |
--------------------------------------------------------------------------------
/infisical/helpers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Infisical/infisical-python/28bf675217ae8713b5480974b195e7a62847fe3d/infisical/helpers/__init__.py
--------------------------------------------------------------------------------
/infisical/helpers/client.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime, timedelta
3 | from typing import TYPE_CHECKING, Union
4 |
5 | from typing_extensions import Literal
6 |
7 | if TYPE_CHECKING:
8 | from infisical import InfisicalClient
9 |
10 | from infisical.logger import logger
11 | from infisical.models.models import SecretBundle
12 | from infisical.services.secret_service import SecretService
13 |
14 |
15 | def get_all_secrets_helper(instance: "InfisicalClient", environment: str, path: str, include_imports: bool, attach_to_os_environ: bool):
16 | try:
17 | if not instance.client_config:
18 | raise Exception("Failed to find client config")
19 |
20 | if not instance.client_config.workspace_config:
21 | instance.client_config.workspace_config = (
22 | SecretService.populate_client_config(
23 | api_request=instance.api_request,
24 | client_config=instance.client_config,
25 | )
26 | )
27 |
28 | secret_bundles = SecretService.get_decrypted_secrets(
29 | api_request=instance.api_request,
30 | workspace_id=instance.client_config.workspace_config.workspace_id,
31 | environment=environment,
32 | path=path,
33 | workspace_key=instance.client_config.workspace_config.workspace_key,
34 | include_imports=include_imports
35 | )
36 |
37 | for secret_bundle in secret_bundles:
38 | cache_key = f"{secret_bundle.type}-{secret_bundle.secret_name}"
39 | instance.cache[cache_key] = secret_bundle
40 | if attach_to_os_environ:
41 | os.environ[secret_bundle.secret_name] = secret_bundle.secret_value
42 |
43 | return secret_bundles
44 | except Exception as exc:
45 | if instance.debug:
46 | logger.exception(exc)
47 |
48 | return [SecretService.get_fallback_secret(secret_name="")]
49 |
50 |
51 | def get_secret_helper(
52 | instance: "InfisicalClient",
53 | secret_name: str,
54 | type: Literal["shared", "personal"],
55 | environment: str,
56 | path: str,
57 | ):
58 | cache_key = f"{type}-{secret_name}"
59 | cached_secret: Union[SecretBundle, None] = None
60 | try:
61 | if not instance.client_config:
62 | raise Exception("Failed to find client config")
63 |
64 | if not instance.client_config.workspace_config:
65 | instance.client_config.workspace_config = (
66 | SecretService.populate_client_config(
67 | api_request=instance.api_request,
68 | client_config=instance.client_config,
69 | )
70 | )
71 |
72 | cached_secret = instance.cache.get(cache_key)
73 |
74 | if cached_secret:
75 | current_time = datetime.now()
76 | cache_expiry_time = cached_secret.last_fetched_at + timedelta(
77 | seconds=instance.client_config.cache_ttl
78 | )
79 |
80 | if current_time < cache_expiry_time:
81 | if instance.debug:
82 | print(f"Returning cached secret: {cached_secret.secret_name}")
83 |
84 | return cached_secret
85 |
86 | secret_bundle = SecretService.get_decrypted_secret(
87 | api_request=instance.api_request,
88 | secret_name=secret_name,
89 | workspace_id=instance.client_config.workspace_config.workspace_id,
90 | environment=environment,
91 | workspace_key=instance.client_config.workspace_config.workspace_key,
92 | type=type,
93 | path=path,
94 | )
95 |
96 | instance.cache[secret_name] = secret_bundle
97 |
98 | return secret_bundle
99 |
100 | except Exception as exc:
101 | if instance.debug:
102 | logger.exception(exc)
103 |
104 | if cached_secret:
105 | if instance.debug:
106 | print(f"Returning cached secret: {cached_secret}")
107 |
108 | return cached_secret
109 |
110 | return SecretService.get_fallback_secret(secret_name=secret_name)
111 |
112 |
113 | def create_secret_helper(
114 | instance: "InfisicalClient",
115 | secret_name: str,
116 | secret_value: str,
117 | type: Literal["shared", "personal"],
118 | environment: str,
119 | path: str,
120 | ):
121 | try:
122 | if not instance.client_config:
123 | raise Exception("Failed to find client config")
124 |
125 | if not instance.client_config.workspace_config:
126 | instance.client_config.workspace_config = (
127 | SecretService.populate_client_config(
128 | api_request=instance.api_request,
129 | client_config=instance.client_config,
130 | )
131 | )
132 |
133 | secret_bundle = SecretService.create_secret(
134 | api_request=instance.api_request,
135 | secret_name=secret_name,
136 | secret_value=secret_value,
137 | workspace_id=instance.client_config.workspace_config.workspace_id,
138 | environment=environment,
139 | workspace_key=instance.client_config.workspace_config.workspace_key,
140 | type=type,
141 | path=path,
142 | )
143 |
144 | cache_key = f"{type}-{secret_name}"
145 | instance.cache[cache_key] = secret_bundle
146 |
147 | return secret_bundle
148 | except Exception as exc:
149 | if instance.debug:
150 | logger.exception(exc)
151 |
152 | return SecretService.get_fallback_secret(secret_name=secret_name)
153 |
154 |
155 | def update_secret_helper(
156 | instance: "InfisicalClient",
157 | secret_name: str,
158 | secret_value: str,
159 | type: Literal["shared", "personal"],
160 | environment: str,
161 | path: str,
162 | ):
163 | try:
164 | if not instance.client_config:
165 | raise Exception("Failed to find client config")
166 |
167 | if not instance.client_config.workspace_config:
168 | instance.client_config.workspace_config = (
169 | SecretService.populate_client_config(
170 | api_request=instance.api_request,
171 | client_config=instance.client_config,
172 | )
173 | )
174 |
175 | secret_bundle = SecretService.update_secret(
176 | api_request=instance.api_request,
177 | secret_name=secret_name,
178 | secret_value=secret_value,
179 | workspace_id=instance.client_config.workspace_config.workspace_id,
180 | environment=environment,
181 | workspace_key=instance.client_config.workspace_config.workspace_key,
182 | type=type,
183 | path=path,
184 | )
185 |
186 | cache_key = f"{type}-{secret_name}"
187 | instance.cache[cache_key] = secret_bundle
188 |
189 | return secret_bundle
190 | except Exception as exc:
191 | if instance.debug:
192 | logger.exception(exc)
193 |
194 | return SecretService.get_fallback_secret(secret_name=secret_name)
195 |
196 |
197 | def delete_secret_helper(
198 | instance: "InfisicalClient",
199 | secret_name: str,
200 | type: Literal["shared", "personal"],
201 | environment: str,
202 | path: str,
203 | ):
204 | try:
205 | if not instance.client_config:
206 | raise Exception("Failed to find client config")
207 |
208 | if not instance.client_config.workspace_config:
209 | instance.client_config.workspace_config = (
210 | SecretService.populate_client_config(
211 | api_request=instance.api_request,
212 | client_config=instance.client_config,
213 | )
214 | )
215 |
216 | secret_bundle = SecretService.delete_secret(
217 | api_request=instance.api_request,
218 | secret_name=secret_name,
219 | workspace_id=instance.client_config.workspace_config.workspace_id,
220 | environment=environment,
221 | workspace_key=instance.client_config.workspace_config.workspace_key,
222 | type=type,
223 | path=path,
224 | )
225 |
226 | cache_key = f"{type}-{secret_name}"
227 | instance.cache[cache_key] = secret_bundle
228 |
229 | return secret_bundle
230 | except Exception as exc:
231 | if instance.debug:
232 | logger.exception(exc)
233 |
234 | return SecretService.get_fallback_secret(secret_name=secret_name)
235 |
--------------------------------------------------------------------------------
/infisical/helpers/secrets.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from infisical.models.models import Secret, SecretBundle
4 |
5 |
6 | def transform_secret_to_secret_bundle(
7 | secret: Secret, secret_name: str, secret_value: str
8 | ) -> SecretBundle:
9 | return SecretBundle(
10 | secret_name=secret_name,
11 | secret_value=secret_value,
12 | version=secret.version,
13 | workspace=secret.workspace,
14 | environment=secret.environment,
15 | type=secret.type,
16 | updated_at=secret.updated_at,
17 | created_at=secret.created_at,
18 | is_fallback=False,
19 | last_fetched_at=datetime.now(),
20 | )
21 |
--------------------------------------------------------------------------------
/infisical/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logger = logging.getLogger("infisical")
4 |
--------------------------------------------------------------------------------
/infisical/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Infisical/infisical-python/28bf675217ae8713b5480974b195e7a62847fe3d/infisical/models/__init__.py
--------------------------------------------------------------------------------
/infisical/models/api.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 | from infisical.models.models import Secret
4 | from pydantic import BaseModel
5 | from typing_extensions import Literal
6 |
7 |
8 | class GetSecretsDTO(BaseModel):
9 | workspace_id: str
10 | environment: str
11 | path: str
12 | include_imports: bool
13 |
14 |
15 | class GetSecretDTO(BaseModel):
16 | secret_name: str
17 | workspace_id: str
18 | environment: str
19 | type: Literal["shared", "personal"]
20 | path: str
21 |
22 |
23 | class CreateSecretDTO(BaseModel):
24 | secret_name: str
25 | workspace_id: str
26 | environment: str
27 | type: Literal["shared", "personal"]
28 | path: str
29 | secret_key_ciphertext: str
30 | secret_key_iv: str
31 | secret_key_tag: str
32 | secret_value_ciphertext: str
33 | secret_value_iv: str
34 | secret_value_tag: str
35 |
36 |
37 | class UpdateSecretDTO(BaseModel):
38 | secret_name: str
39 | workspace_id: str
40 | environment: str
41 | type: Literal["shared", "personal"]
42 | path: str
43 | secret_value_ciphertext: str
44 | secret_value_iv: str
45 | secret_value_tag: str
46 |
47 |
48 | class DeleteSecretDTO(BaseModel):
49 | secret_name: str
50 | workspace_id: str
51 | environment: str
52 | type: Literal["shared", "personal"]
53 | path: str
54 |
55 |
56 | class SecretImport(BaseModel):
57 | secretPath: str
58 | folderId: str
59 | environment: str
60 | secrets: List[Secret]
61 |
62 | class SecretsResponse(BaseModel):
63 | secrets: List[Secret]
64 | imports: Optional[List[SecretImport]]
65 |
66 |
67 | class SecretResponse(BaseModel):
68 | secret: Secret
69 |
--------------------------------------------------------------------------------
/infisical/models/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel, Field
5 | from typing_extensions import Literal
6 |
7 |
8 | class Secret(BaseModel):
9 | id: str = Field(..., alias="id")
10 | version: int
11 | workspace: str
12 | user: Optional[str]
13 | type: Literal["shared", "personal"]
14 | environment: str
15 | secret_key_ciphertext: str = Field(..., alias="secretKeyCiphertext")
16 | secret_key_iv: str = Field(..., alias="secretKeyIV")
17 | secret_key_tag: str = Field(..., alias="secretKeyTag")
18 | secret_value_ciphertext: str = Field(..., alias="secretValueCiphertext")
19 | secret_value_iv: str = Field(..., alias="secretValueIV")
20 | secret_value_tag: str = Field(..., alias="secretValueTag")
21 | created_at: datetime = Field(..., alias="createdAt")
22 | updated_at: datetime = Field(..., alias="updatedAt")
23 |
24 |
25 | class SecretBundle(BaseModel):
26 | secret_name: str
27 | secret_value: Optional[str]
28 | version: Optional[int]
29 | # workspace: Optional[str]
30 | # environment: Optional[str]
31 | type: Optional[Literal["shared", "personal"]]
32 | created_at: Optional[datetime] = Field(None, alias="createdAt")
33 | updated_at: Optional[datetime] = Field(None, alias="updatedAt")
34 | is_fallback: bool
35 | last_fetched_at: datetime
36 |
37 |
38 | class ServiceTokenData(BaseModel):
39 | id: str = Field(..., alias="id")
40 | name: str
41 | workspace: str
42 | environment: str
43 | user: str
44 | service_account: str
45 | last_used: datetime = Field()
46 | expires_at: datetime = Field(..., alias="expiresAt")
47 | encrypted_key: str = Field(..., alias="encryptedKey")
48 | iv: str
49 | tag: str
50 | created_at: datetime = Field(..., alias="createdAt")
51 | updated_at: datetime = Field(..., alias="updatedAt")
52 |
--------------------------------------------------------------------------------
/infisical/models/secret_service.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Optional, Union
2 |
3 | from pydantic import BaseModel
4 | from requests import Session
5 | from typing_extensions import Literal
6 |
7 | class WorkspaceConfig(BaseModel):
8 | workspace_id: str
9 | workspace_key: str
10 |
11 | class ServiceTokenCredentials(BaseModel):
12 | service_token_key: str
13 |
14 | class ServiceTokenV3Credentials(BaseModel):
15 | public_key: str
16 | private_key: str
17 |
18 | class ClientConfig(BaseModel):
19 | auth_mode: Literal["service_token", "service_token_v3"]
20 | credentials: Union[ServiceTokenCredentials, ServiceTokenV3Credentials]
21 | workspace_config: Optional[WorkspaceConfig]
22 | cache_ttl: int
23 |
--------------------------------------------------------------------------------
/infisical/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Infisical/infisical-python/28bf675217ae8713b5480974b195e7a62847fe3d/infisical/services/__init__.py
--------------------------------------------------------------------------------
/infisical/services/secret_service.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 | from typing import List
4 |
5 | from infisical.api.create_secret import create_secret_req
6 | from infisical.api.delete_secret import delete_secret_req
7 | from infisical.api.get_secret import get_secret_req
8 | from infisical.api.get_secrets import get_secrets_req
9 | from infisical.api.get_service_token_data import get_service_token_data_req
10 | from infisical.api.get_service_token_data_key import get_service_token_data_key_req
11 | from infisical.api.update_secret import update_secret_req
12 | from infisical.helpers.secrets import transform_secret_to_secret_bundle
13 | from infisical.models.api import (
14 | CreateSecretDTO,
15 | DeleteSecretDTO,
16 | GetSecretDTO,
17 | GetSecretsDTO,
18 | UpdateSecretDTO,
19 | )
20 | from infisical.models.models import SecretBundle
21 | from infisical.models.secret_service import ClientConfig, WorkspaceConfig
22 |
23 | # from infisical.utils.crypto import decrypt_symmetric, encrypt_symmetric
24 | from infisical.utils.crypto import (
25 | decrypt_symmetric_128_bit_hex_key_utf8,
26 | encrypt_symmetric_128_bit_hex_key_utf8,
27 | decrypt_asymmetric
28 | )
29 | from requests import Session
30 | from typing_extensions import Literal
31 |
32 |
33 | class SecretService:
34 | @staticmethod
35 | def populate_client_config(
36 | api_request: Session, client_config: ClientConfig
37 | ) -> WorkspaceConfig:
38 | if client_config.auth_mode == "service_token":
39 | service_token_details = get_service_token_data_req(api_request)
40 | workspace_key = decrypt_symmetric_128_bit_hex_key_utf8(
41 | ciphertext=service_token_details.encrypted_key,
42 | iv=service_token_details.iv,
43 | tag=service_token_details.tag,
44 | key=client_config.credentials.service_token_key,
45 | )
46 |
47 | return WorkspaceConfig(
48 | workspace_id=service_token_details.workspace,
49 | workspace_key=workspace_key,
50 | )
51 |
52 | if client_config.auth_mode == "service_token_v3":
53 | service_token_key_details = get_service_token_data_key_req(api_request)
54 | workspace_key = decrypt_asymmetric(
55 | ciphertext=service_token_key_details.key.encrypted_key,
56 | nonce=service_token_key_details.key.nonce,
57 | public_key=service_token_key_details.key.public_key,
58 | private_key=client_config.credentials.private_key
59 | )
60 |
61 | return WorkspaceConfig(
62 | workspace_id=service_token_key_details.key.workspace,
63 | workspace_key=workspace_key
64 | )
65 |
66 | @staticmethod
67 | def get_fallback_secret(secret_name: str) -> SecretBundle:
68 | return SecretBundle(
69 | secret_name=secret_name,
70 | secret_value=os.environ[secret_name],
71 | is_fallback=True,
72 | last_fetched_at=datetime.now(),
73 | )
74 |
75 | @staticmethod
76 | def get_decrypted_secrets(
77 | api_request: Session,
78 | workspace_key: str,
79 | workspace_id: str,
80 | environment: str,
81 | path: str,
82 | include_imports: bool
83 | ) -> List[SecretBundle]:
84 | options = GetSecretsDTO(
85 | workspace_id=workspace_id, environment=environment, path=path, include_imports=include_imports
86 | )
87 |
88 | encrypted_secrets, secret_imports = get_secrets_req(api_request, options)
89 |
90 | secret_bundles: List[SecretBundle] = []
91 |
92 | for encrypted_secret in encrypted_secrets:
93 | secret_name = decrypt_symmetric_128_bit_hex_key_utf8(
94 | ciphertext=encrypted_secret.secret_key_ciphertext,
95 | iv=encrypted_secret.secret_key_iv,
96 | tag=encrypted_secret.secret_key_tag,
97 | key=workspace_key,
98 | )
99 |
100 | secret_value = decrypt_symmetric_128_bit_hex_key_utf8(
101 | ciphertext=encrypted_secret.secret_value_ciphertext,
102 | iv=encrypted_secret.secret_value_iv,
103 | tag=encrypted_secret.secret_value_tag,
104 | key=workspace_key,
105 | )
106 |
107 | secret_bundles.append(
108 | transform_secret_to_secret_bundle(
109 | secret=encrypted_secret,
110 | secret_name=secret_name,
111 | secret_value=secret_value,
112 | )
113 | )
114 |
115 | for secret_import in secret_imports:
116 | for encrypted_secret in secret_import.secrets:
117 | secret_name = decrypt_symmetric_128_bit_hex_key_utf8(
118 | ciphertext=encrypted_secret.secret_key_ciphertext,
119 | iv=encrypted_secret.secret_key_iv,
120 | tag=encrypted_secret.secret_key_tag,
121 | key=workspace_key,
122 | )
123 |
124 | secret_value = decrypt_symmetric_128_bit_hex_key_utf8(
125 | ciphertext=encrypted_secret.secret_value_ciphertext,
126 | iv=encrypted_secret.secret_value_iv,
127 | tag=encrypted_secret.secret_value_tag,
128 | key=workspace_key,
129 | )
130 |
131 | secret_bundles.append(
132 | transform_secret_to_secret_bundle(
133 | secret=encrypted_secret,
134 | secret_name=secret_name,
135 | secret_value=secret_value,
136 | )
137 | )
138 |
139 | return secret_bundles
140 |
141 | @staticmethod
142 | def get_decrypted_secret(
143 | api_request: Session,
144 | secret_name: str,
145 | workspace_id: str,
146 | environment: str,
147 | workspace_key: str,
148 | type: Literal["shared", "personal"],
149 | path: str,
150 | ):
151 | options = GetSecretDTO(
152 | secret_name=secret_name,
153 | workspace_id=workspace_id,
154 | environment=environment,
155 | type=type,
156 | path=path,
157 | )
158 |
159 | encrypted_secret = get_secret_req(
160 | api_request,
161 | options,
162 | )
163 |
164 | secret_value = decrypt_symmetric_128_bit_hex_key_utf8(
165 | ciphertext=encrypted_secret.secret.secret_value_ciphertext,
166 | iv=encrypted_secret.secret.secret_value_iv,
167 | tag=encrypted_secret.secret.secret_value_tag,
168 | key=workspace_key,
169 | )
170 |
171 | return transform_secret_to_secret_bundle(
172 | secret=encrypted_secret.secret,
173 | secret_name=secret_name,
174 | secret_value=secret_value,
175 | )
176 |
177 | @staticmethod
178 | def create_secret(
179 | api_request: Session,
180 | workspace_key: str,
181 | workspace_id: str,
182 | environment: str,
183 | type: Literal["shared", "personal"],
184 | secret_name: str,
185 | secret_value: str,
186 | path: str,
187 | ):
188 | (
189 | secret_key_ciphertext,
190 | secret_key_iv,
191 | secret_key_tag,
192 | ) = encrypt_symmetric_128_bit_hex_key_utf8(
193 | plaintext=secret_name, key=workspace_key
194 | )
195 |
196 | (
197 | secret_value_ciphertext,
198 | secret_value_iv,
199 | secret_value_tag,
200 | ) = encrypt_symmetric_128_bit_hex_key_utf8(
201 | plaintext=secret_value, key=workspace_key
202 | )
203 |
204 | options = CreateSecretDTO(
205 | secret_name=secret_name,
206 | workspace_id=workspace_id,
207 | environment=environment,
208 | type=type,
209 | path=path,
210 | secret_key_ciphertext=secret_key_ciphertext,
211 | secret_key_iv=secret_key_iv,
212 | secret_key_tag=secret_key_tag,
213 | secret_value_ciphertext=secret_value_ciphertext,
214 | secret_value_iv=secret_value_iv,
215 | secret_value_tag=secret_value_tag,
216 | )
217 |
218 | encrypted_secret = create_secret_req(api_request, options)
219 |
220 | return transform_secret_to_secret_bundle(
221 | secret=encrypted_secret.secret,
222 | secret_name=secret_name,
223 | secret_value=secret_value,
224 | )
225 |
226 | @staticmethod
227 | def update_secret(
228 | api_request: Session,
229 | workspace_key: str,
230 | workspace_id: str,
231 | environment: str,
232 | type: Literal["shared", "personal"],
233 | secret_name: str,
234 | secret_value: str,
235 | path: str,
236 | ):
237 | (
238 | secret_value_ciphertext,
239 | secret_value_iv,
240 | secret_value_tag,
241 | ) = encrypt_symmetric_128_bit_hex_key_utf8(
242 | plaintext=secret_value, key=workspace_key
243 | )
244 |
245 | options = UpdateSecretDTO(
246 | secret_name=secret_name,
247 | workspace_id=workspace_id,
248 | environment=environment,
249 | type=type,
250 | secret_value_ciphertext=secret_value_ciphertext,
251 | secret_value_iv=secret_value_iv,
252 | secret_value_tag=secret_value_tag,
253 | path=path,
254 | )
255 |
256 | encrypted_secret = update_secret_req(api_request, options)
257 | return transform_secret_to_secret_bundle(
258 | secret=encrypted_secret.secret,
259 | secret_name=secret_name,
260 | secret_value=secret_value,
261 | )
262 |
263 | @staticmethod
264 | def delete_secret(
265 | api_request: Session,
266 | workspace_key: str,
267 | workspace_id: str,
268 | environment: str,
269 | type: Literal["shared", "personal"],
270 | path: str,
271 | secret_name: str,
272 | ):
273 | options = DeleteSecretDTO(
274 | secret_name=secret_name,
275 | workspace_id=workspace_id,
276 | environment=environment,
277 | type=type,
278 | path=path,
279 | )
280 |
281 | encrypted_secret = delete_secret_req(api_request, options)
282 |
283 | secret_value = decrypt_symmetric_128_bit_hex_key_utf8(
284 | ciphertext=encrypted_secret.secret.secret_value_ciphertext,
285 | iv=encrypted_secret.secret.secret_value_iv,
286 | tag=encrypted_secret.secret.secret_value_tag,
287 | key=workspace_key,
288 | )
289 |
290 | return transform_secret_to_secret_bundle(
291 | secret=encrypted_secret.secret,
292 | secret_name=secret_name,
293 | secret_value=secret_value,
294 | )
295 |
--------------------------------------------------------------------------------
/infisical/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Infisical/infisical-python/28bf675217ae8713b5480974b195e7a62847fe3d/infisical/utils/__init__.py
--------------------------------------------------------------------------------
/infisical/utils/crypto.py:
--------------------------------------------------------------------------------
1 | from base64 import b64decode, b64encode
2 | from typing import Tuple, Union
3 |
4 | from Cryptodome.Cipher import AES
5 | from Cryptodome.Random import get_random_bytes
6 | from nacl import public, utils
7 |
8 | Base64String = str
9 | Buffer = Union[bytes, bytearray, memoryview]
10 | import binascii
11 |
12 |
13 | def encrypt_asymmetric(
14 | plaintext: Union[Buffer, str],
15 | public_key: Union[Buffer, Base64String, public.PublicKey],
16 | private_key: Union[Buffer, Base64String, public.PrivateKey],
17 | ) -> Tuple[Base64String, Base64String]:
18 | """Performs asymmetric encryption of the ``plaintext`` with x25519-xsalsa20-poly1305
19 | algorithm with the given parameters. Each of those params should be either the raw value in bytes
20 | or a base64 string.
21 |
22 | :param plaintext: The text to encrypt
23 | :param public_key: The public key
24 | :param private_key: The private key
25 | :raises ValueError: If ``plaintext``, ``public_key`` or ``private_key`` are empty
26 | :return: A tuple containing the ciphered text and the random nonce used for encryption
27 | """
28 | if (not isinstance(public_key, public.PublicKey) and len(public_key) == 0) or (
29 | not isinstance(private_key, public.PrivateKey) and len(private_key) == 0
30 | ):
31 | raise ValueError("Public key and private key cannot be empty!")
32 |
33 | m_plaintext = (
34 | str.encode(plaintext, "utf-8") if isinstance(plaintext, str) else plaintext
35 | )
36 | m_public_key = (
37 | b64decode(public_key) if isinstance(public_key, Base64String) else public_key
38 | )
39 | m_public_key = (
40 | public.PublicKey(m_public_key)
41 | if isinstance(m_public_key, (bytes, bytearray, memoryview))
42 | else m_public_key
43 | )
44 | m_private_key = (
45 | b64decode(private_key) if isinstance(private_key, Base64String) else private_key
46 | )
47 | m_private_key = (
48 | public.PrivateKey(m_private_key)
49 | if isinstance(m_private_key, (bytes, bytearray, memoryview))
50 | else m_private_key
51 | )
52 |
53 | nonce = utils.random(24)
54 | box = public.Box(m_private_key, m_public_key)
55 | ciphertext = box.encrypt(m_plaintext, nonce).ciphertext
56 |
57 | return (b64encode(ciphertext).decode("utf-8"), b64encode(nonce).decode("utf-8"))
58 |
59 |
60 | def decrypt_asymmetric(
61 | ciphertext: Union[Buffer, Base64String],
62 | nonce: Union[Buffer, Base64String],
63 | public_key: Union[Buffer, Base64String, public.PublicKey],
64 | private_key: Union[Buffer, Base64String, public.PrivateKey],
65 | ) -> str:
66 | """Performs asymmetric decryption of the ``ciphertext`` with x25519-xsalsa20-poly1305
67 | algorithm with the given parameters. Each of those params should be either the raw value in bytes
68 | or a base64 string.
69 |
70 | :param ciphertext: The ciphered text to decrypt
71 | :param nonce: The nonce used for encryption
72 | :param public_key: The public key
73 | :param private_key: The private key
74 | :raises ValueError: If ``ciphertext``, ``nonce``, ``public_key`` or ``private_key`` are empty
75 | :return: The deciphered text
76 | """
77 | if (
78 | len(ciphertext) == 0
79 | or len(nonce) == 0
80 | or (not isinstance(public_key, public.PublicKey) and len(public_key) == 0)
81 | or (not isinstance(private_key, public.PrivateKey) and len(private_key) == 0)
82 | ):
83 | raise ValueError(
84 | "Public key, private key, ciphertext and nonce cannot be empty!"
85 | )
86 |
87 | m_ciphertext = (
88 | b64decode(ciphertext) if isinstance(ciphertext, Base64String) else ciphertext
89 | )
90 | m_nonce = b64decode(nonce) if isinstance(nonce, Base64String) else nonce
91 | m_public_key = (
92 | b64decode(public_key) if isinstance(public_key, Base64String) else public_key
93 | )
94 | m_public_key = (
95 | public.PublicKey(m_public_key)
96 | if isinstance(m_public_key, (bytes, bytearray, memoryview))
97 | else m_public_key
98 | )
99 | m_private_key = (
100 | b64decode(private_key) if isinstance(private_key, Base64String) else private_key
101 | )
102 | m_private_key = (
103 | public.PrivateKey(m_private_key)
104 | if isinstance(m_private_key, (bytes, bytearray, memoryview))
105 | else m_private_key
106 | )
107 |
108 | box = public.Box(m_private_key, m_public_key)
109 | plaintext = box.decrypt(m_ciphertext, m_nonce)
110 |
111 | return plaintext.decode("utf-8")
112 |
113 |
114 | def create_symmetric_key_helper():
115 | return b64encode(get_random_bytes(32)).decode("utf-8")
116 |
117 |
118 | def encrypt_symmetric_helper(plaintext: str, key: str):
119 | IV_BYTES_SIZE = 12
120 | iv = get_random_bytes(12)
121 |
122 | cipher = AES.new(b64decode(key), AES.MODE_GCM, nonce=iv)
123 |
124 | ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode("utf-8"))
125 |
126 | return (
127 | b64encode(ciphertext).decode("utf-8"),
128 | b64encode(iv).decode("utf-8"),
129 | b64encode(tag).decode("utf-8"),
130 | )
131 |
132 |
133 | def decrypt_symmetric_helper(ciphertext: str, key: str, iv: str, tag: str):
134 | cipher = AES.new(b64decode(key), AES.MODE_GCM, nonce=b64decode(iv))
135 | plaintext = cipher.decrypt_and_verify(b64decode(ciphertext), b64decode(tag))
136 |
137 | return plaintext.decode("utf-8")
138 |
139 |
140 | def encrypt_symmetric_128_bit_hex_key_utf8(
141 | plaintext: str, key: str
142 | ) -> Tuple[Base64String, Base64String, Base64String]:
143 | """Encrypts the ``plaintext`` with aes-256-gcm using the given ``key``.
144 | The key should be either the raw value in bytes or a base64 string.
145 |
146 | :param plaintext: text to encrypt
147 | :param key: UTF-8, 128-bit AES key used for encryption
148 | :raises ValueError: If either ``plaintext`` or ``key`` is empty
149 | :return: Ciphered text
150 | """
151 | if len(key) == 0:
152 | raise ValueError("The given key is empty!")
153 |
154 | BLOCK_SIZE_BYTES = 16
155 |
156 | iv = get_random_bytes(BLOCK_SIZE_BYTES)
157 | cipher = AES.new(bytes(key, "utf-8"), AES.MODE_GCM, nonce=iv)
158 |
159 | ciphertext, tag = cipher.encrypt_and_digest(str.encode(plaintext, "utf-8"))
160 |
161 | return (
162 | b64encode(ciphertext).decode("utf-8"),
163 | b64encode(iv).decode("utf-8"),
164 | b64encode(tag).decode("utf-8"),
165 | )
166 |
167 |
168 | def decrypt_symmetric_128_bit_hex_key_utf8(
169 | key: str, ciphertext: str, tag: str, iv: str
170 | ) -> str:
171 | """Decrypts the ``ciphertext`` with aes-256-gcm using ``iv``, ``tag``
172 | and ``key``.
173 |
174 | :param key: UTF-8, 128-bit hex AES key
175 | :param ciphertext: base64 ciphered text to decrypt
176 | :param tag: base64 tag/mac used for verification
177 | :param iv: base64 nonce
178 | :raises ValueError: If ``ciphertext``, ``iv``, ``tag`` or ``key`` are empty or tag/mac does not match
179 | :return: Deciphered text
180 | """
181 | if len(tag) == 0 or len(iv) == 0 or len(key) == 0:
182 | raise ValueError("One of the given parameter is empty!")
183 |
184 | try:
185 | key = bytes(key, "utf-8")
186 | iv = b64decode(iv)
187 | tag = b64decode(tag)
188 | ciphertext = b64decode(ciphertext)
189 |
190 | cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
191 | plaintext = cipher.decrypt_and_verify(ciphertext, tag)
192 |
193 | return plaintext.decode("utf-8")
194 | except ValueError:
195 | raise ValueError("Incorrect decryption or MAC check failed")
196 |
--------------------------------------------------------------------------------
/infisical/utils/http.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Optional, Union
2 | from urllib.parse import urljoin
3 |
4 | import requests
5 | from requests.adapters import HTTPAdapter
6 | from urllib3.util import Retry
7 |
8 | DEFAULT_TIMEOUT = 40 # seconds
9 |
10 |
11 | class BaseUrlSession(requests.Session):
12 | base_url = ""
13 |
14 | def __init__(self, base_url: Optional[str] = None) -> None:
15 | if base_url:
16 | self.base_url = base_url
17 | super().__init__()
18 |
19 | def request(
20 | self,
21 | method: Union[str, bytes],
22 | url: Any,
23 | *args: Any,
24 | **kwargs: Any,
25 | ) -> requests.Response:
26 | url = self.create_url(url)
27 | return super().request(method, url, *args, **kwargs)
28 |
29 | def prepare_request(self, request: requests.Request) -> requests.PreparedRequest:
30 | request.url = self.create_url(request.url)
31 | return super().prepare_request(request)
32 |
33 | def create_url(self, url: str) -> str:
34 | return urljoin(self.base_url, url)
35 |
36 |
37 | class TimeoutHTTPAdapter(HTTPAdapter):
38 | def __init__(self, *args: Any, **kwargs: Any) -> None:
39 | self.timeout = DEFAULT_TIMEOUT
40 | if "timeout" in kwargs:
41 | self.timeout = kwargs["timeout"]
42 | del kwargs["timeout"]
43 | super().__init__(*args, **kwargs)
44 |
45 | def send(
46 | self, request: requests.PreparedRequest, *args: Any, **kwargs: Any
47 | ) -> requests.Response:
48 | timeout = kwargs.get("timeout")
49 | if timeout is None:
50 | kwargs["timeout"] = self.timeout
51 | return super().send(request, *args, **kwargs)
52 |
53 |
54 | def get_http_client(
55 | base_url: Optional[str],
56 | retries: int = 3,
57 | backoff_factor: int = 1,
58 | timeout: int = DEFAULT_TIMEOUT,
59 | ) -> BaseUrlSession:
60 | """Returns a pre-configured :class:`requests.Session` with a optional ``base_url``
61 | and some sane options for timeout and retry handling.
62 |
63 | :param base_url: (optional) A base url used for each request made with this client
64 | :param retries: The number of retries to do if request fail, defaults to 3
65 | :param backoff_factor: A backoff factor to apply between attempts after the second try, defaults to 1
66 | :param timeout: The timeout in seconds, defaults to 40
67 | :return: A ready-to-use instance of :class:`requests.Session`
68 | """
69 | retry_strategy = Retry(
70 | total=retries,
71 | backoff_factor=backoff_factor,
72 | status_forcelist=[429, 500, 502, 503, 504],
73 | )
74 | adapter = TimeoutHTTPAdapter(max_retries=retry_strategy, timeout=timeout)
75 |
76 | http = BaseUrlSession(base_url=base_url)
77 | http.mount("https://", adapter)
78 | http.mount("http://", adapter)
79 |
80 | return http
81 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "infisical"
7 | description = 'Official Infisical SDK for Python'
8 | readme = "README.md"
9 | requires-python = ">=3.7"
10 | license = "MIT"
11 | authors = []
12 | maintainers = [
13 | { name = "Yohann MARTIN", email = "contact@codexus.fr" },
14 | { name = "Tony Dang", email = "tony@infisical.com"}
15 | ]
16 | classifiers = [
17 | "Development Status :: 5 - Production/Stable",
18 | "Operating System :: OS Independent",
19 | "Topic :: Software Development :: Libraries :: Application Frameworks",
20 | "Topic :: Software Development :: Libraries :: Python Modules",
21 | "Topic :: Software Development :: Libraries",
22 | "Topic :: Software Development",
23 | "Typing :: Typed",
24 | "Intended Audience :: Developers",
25 | "License :: OSI Approved :: MIT License",
26 | "Programming Language :: Python",
27 | "Programming Language :: Python :: 3.7",
28 | "Programming Language :: Python :: 3.8",
29 | "Programming Language :: Python :: 3.9",
30 | "Programming Language :: Python :: 3.10",
31 | "Programming Language :: Python :: 3.11",
32 | "Programming Language :: Python :: Implementation :: CPython",
33 | "Programming Language :: Python :: Implementation :: PyPy",
34 | ]
35 | dependencies = [
36 | "requests ==2.31.0",
37 | "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0",
38 | "pycryptodomex >=3.17,<4.0.0",
39 | "pynacl >=1.5.0,<2.0.0"
40 | ]
41 | dynamic = ["version"]
42 |
43 | [project.urls]
44 | Documentation = "https://github.com/Infisical/infisical-python#readme"
45 | Issues = "https://github.com/Infisical/infisical-python/issues"
46 | Source = "https://github.com/Infisical/infisical-python"
47 |
48 | [project.optional-dependencies]
49 | test = [
50 | "pytest >=7.1.3,<8.0.0",
51 | "coverage[toml] >= 6.5.0,< 8.0",
52 | "responses ==0.23.1"
53 | ]
54 | dev = [
55 | "mypy ==1.1.1",
56 | "ruff ==0.0.261",
57 | "black ==23.3.0",
58 | "isort >=5.0.6,<6.0.0",
59 | "devtools[pygments] ==0.11.0",
60 |
61 | "types-requests ==2.28.11.17"
62 | ]
63 |
64 | [tool.hatch.version]
65 | path = "infisical/__version__.py"
66 |
67 | [tool.hatch.build.targets.sdist]
68 | exclude = [
69 | "/.github",
70 | "/.vscode",
71 | ]
72 |
73 | [tool.isort]
74 | profile = "black"
75 | known_third_party = ["infisical", "pydantic", "Cryptodome", "nacl", "responses"]
76 |
77 | [tool.mypy]
78 | strict = true
79 |
80 | [tool.coverage.run]
81 | parallel = true
82 | source = [
83 | "tests",
84 | "infisical"
85 | ]
86 |
87 | [tool.coverage.report]
88 | exclude_lines = [
89 | "no cov",
90 | "if __name__ == .__main__.:",
91 | "if TYPE_CHECKING:",
92 | ]
93 |
94 | [tool.ruff]
95 | target-version = "py37"
96 | select = [
97 | "E", # pycodestyle errors
98 | "W", # pycodestyle warnings
99 | "F", # pyflakes
100 | # "I", # isort
101 | "C", # flake8-comprehensions
102 | "B", # flake8-bugbear
103 | ]
104 | ignore = [
105 | "E501", # line too long, handled by black
106 | "B008", # do not perform function calls in argument defaults
107 | "C901", # too complex
108 | ]
109 |
110 | [tool.ruff.per-file-ignores]
111 | "__init__.py" = ["F401"]
112 |
113 | [tool.ruff.isort]
114 | known-third-party = ["infisical"]
115 |
--------------------------------------------------------------------------------
/scripts/format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 | set -x
3 |
4 | ruff check infisical tests --fix
5 | black infisical tests
6 | isort infisical tests
7 |
--------------------------------------------------------------------------------
/scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | mypy infisical
7 | ruff check infisical tests
8 | black infisical tests --check
9 | isort infisical tests --check-only
10 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | coverage run -m pytest tests ${@}
7 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2023-present Yohann MARTIN
2 | #
3 | # SPDX-License-Identifier: MIT
4 |
--------------------------------------------------------------------------------
/tests/test_client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Infisical/infisical-python/28bf675217ae8713b5480974b195e7a62847fe3d/tests/test_client/__init__.py
--------------------------------------------------------------------------------
/tests/test_client/test_infisical_client.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | from infisical import InfisicalClient
5 |
6 |
7 | @pytest.fixture(scope="module")
8 | def client():
9 | infisical_client = InfisicalClient(
10 | token=os.environ.get("INFISICAL_TOKEN"), token_json=os.environ.get("INFISICAL_TOKEN_JSON"), site_url=os.environ.get("SITE_URL"), debug=True
11 | )
12 |
13 | infisical_client.create_secret("KEY_ONE", "KEY_ONE_VAL")
14 | infisical_client.create_secret("KEY_ONE", "KEY_ONE_VAL_PERSONAL", type="personal")
15 | infisical_client.create_secret("KEY_TWO", "KEY_TWO_VAL")
16 |
17 | # infisical_client.create_secret("TESTOO", "sssss", path="/foo", environment="dev")
18 |
19 | yield infisical_client
20 |
21 | infisical_client.delete_secret("KEY_ONE")
22 | infisical_client.delete_secret("KEY_TWO")
23 | infisical_client.delete_secret("KEY_THREE")
24 |
25 | def test_get_overriden_personal_secret(client: InfisicalClient):
26 | secret = client.get_secret("KEY_ONE")
27 | assert secret.secret_name == "KEY_ONE"
28 | assert secret.secret_value == "KEY_ONE_VAL_PERSONAL"
29 | assert secret.type == "personal"
30 |
31 |
32 | def test_get_shared_secret_specified(client: InfisicalClient):
33 | secret = client.get_secret("KEY_ONE", type="shared")
34 | assert secret.secret_name == "KEY_ONE"
35 | assert secret.secret_value == "KEY_ONE_VAL"
36 | assert secret.type == "shared"
37 |
38 |
39 | def test_get_shared_secret(client: InfisicalClient):
40 | secret = client.get_secret("KEY_TWO")
41 | assert secret.secret_name == "KEY_TWO"
42 | assert secret.secret_value == "KEY_TWO_VAL"
43 | assert secret.type == "shared"
44 |
45 |
46 | def test_create_shared_secret(client: InfisicalClient):
47 | secret = client.create_secret("KEY_THREE", "KEY_THREE_VAL")
48 | assert secret.secret_name == "KEY_THREE"
49 | assert secret.secret_value == "KEY_THREE_VAL"
50 | assert secret.type == "shared"
51 |
52 |
53 | def test_create_personal_secret(client: InfisicalClient):
54 | client.create_secret("KEY_FOUR", "KEY_FOUR_VAL")
55 | personal_secret = client.create_secret(
56 | "KEY_FOUR", "KEY_FOUR_VAL_PERSONAL", type="personal"
57 | )
58 |
59 | assert personal_secret.secret_name == "KEY_FOUR"
60 | assert personal_secret.secret_value == "KEY_FOUR_VAL_PERSONAL"
61 | assert personal_secret.type == "personal"
62 |
63 |
64 | def test_update_shared_secret(client: InfisicalClient):
65 | secret = client.update_secret("KEY_THREE", "FOO")
66 |
67 | assert secret.secret_name == "KEY_THREE"
68 | assert secret.secret_value == "FOO"
69 | assert secret.type == "shared"
70 |
71 |
72 | def test_update_personal_secret(client: InfisicalClient):
73 | secret = client.update_secret("KEY_FOUR", "BAR", type="personal")
74 | assert secret.secret_name == "KEY_FOUR"
75 | assert secret.secret_value == "BAR"
76 | assert secret.type == "personal"
77 |
78 |
79 | def test_delete_personal_secret(client: InfisicalClient):
80 | secret = client.delete_secret("KEY_FOUR", type="personal")
81 | assert secret.secret_name == "KEY_FOUR"
82 | assert secret.secret_value == "BAR"
83 | assert secret.type == "personal"
84 |
85 |
86 | def test_delete_shared_secret(client: InfisicalClient):
87 | secret = client.delete_secret("KEY_FOUR")
88 | assert secret.secret_name == "KEY_FOUR"
89 | assert secret.secret_value == "KEY_FOUR_VAL"
90 | assert secret.type == "shared"
91 |
92 |
93 | def test_encrypt_decrypt_symmetric(client: InfisicalClient):
94 | plaintext = "The quick brown fox jumps over the lazy dog"
95 | key = client.create_symmetric_key()
96 |
97 | ciphertext, iv, tag = client.encrypt_symmetric(plaintext, key)
98 | cleartext = client.decrypt_symmetric(ciphertext, key, iv, tag)
99 | assert plaintext == cleartext
100 |
--------------------------------------------------------------------------------
/tests/test_infisical.py:
--------------------------------------------------------------------------------
1 | from dotenv import load_dotenv
2 |
3 | load_dotenv()
4 |
--------------------------------------------------------------------------------