Skip to content

Commit 396506a

Browse files
committed
feat: crypto module
1 parent 224c16c commit 396506a

4 files changed

Lines changed: 241 additions & 2 deletions

File tree

infisical_sdk/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .client import InfisicalSDKClient # noqa
22
from .infisical_requests import InfisicalError # noqa
3-
from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret # noqa
3+
from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret # noqa
4+
from .crypto import create_symmetric_key_helper, encrypt_symmetric_helper, decrypt_symmetric_helper # noqa

infisical_sdk/client.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,31 @@
1515
from .api_types import ListSecretsResponse, MachineIdentityLoginResponse
1616
from .api_types import SingleSecretResponse, BaseSecret
1717

18+
from .crypto import (
19+
create_symmetric_key_helper,
20+
decrypt_symmetric_helper,
21+
encrypt_symmetric_helper,
22+
)
23+
1824

1925
class InfisicalSDKClient:
20-
def __init__(self, host: str, token: str = None):
26+
def __init__(self, host: str = None, token: str = None):
27+
28+
if host is None:
29+
host = "https://app.infisical.com"
30+
2131
self.host = host
32+
33+
if host.endswith("/api"):
34+
host = host[:-4]
35+
2236
self.access_token = token
2337

2438
self.api = InfisicalRequests(host=host, token=token)
2539

2640
self.auth = Auth(self)
2741
self.secrets = V3RawSecrets(self)
42+
self.crypto = Cryptography(self)
2843

2944
def set_token(self, token: str):
3045
"""
@@ -343,3 +358,22 @@ def delete_secret_by_name(
343358
)
344359

345360
return result.data.secret
361+
362+
363+
class Cryptography:
364+
def __init__(self, client: InfisicalSDKClient) -> None:
365+
self.client = client
366+
367+
def create_symmetric_key(self) -> str:
368+
"""Create a base64-encoded, 256-bit symmetric key"""
369+
return create_symmetric_key_helper()
370+
371+
def encrypt_symmetric(self, plaintext: str, key: str):
372+
"""Encrypt the plaintext `plaintext` with the (base64) 256-bit secret key `key`"""
373+
return encrypt_symmetric_helper(plaintext, key)
374+
375+
def decrypt_symmetric(self, ciphertext: str, key: str, iv: str, tag: str):
376+
"""Decrypt the ciphertext `ciphertext` with the (base64) 256-bit secret key `key`,
377+
provided `iv` and `tag`"""
378+
379+
return decrypt_symmetric_helper(ciphertext, key, iv, tag)

infisical_sdk/crypto.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
11+
12+
def encrypt_asymmetric(
13+
plaintext: Union[Buffer, str],
14+
public_key: Union[Buffer, Base64String, public.PublicKey],
15+
private_key: Union[Buffer, Base64String, public.PrivateKey],
16+
) -> Tuple[Base64String, Base64String]:
17+
"""Performs asymmetric encryption of the ``plaintext`` with x25519-xsalsa20-poly1305
18+
algorithm with the given parameters.
19+
Each of those params should be either the raw value in bytes or a base64 string.
20+
21+
:param plaintext: The text to encrypt
22+
:param public_key: The public key
23+
:param private_key: The private key
24+
25+
:raises ValueError: If ``plaintext``, ``public_key`` or ``private_key`` are empty
26+
27+
:return: A tuple containing the ciphered text and the random nonce used for encryption
28+
"""
29+
if (not isinstance(public_key, public.PublicKey) and len(public_key) == 0) or (
30+
not isinstance(private_key, public.PrivateKey) and len(private_key) == 0
31+
):
32+
raise ValueError("Public key and private key cannot be empty!")
33+
34+
m_plaintext = (
35+
str.encode(plaintext, "utf-8") if isinstance(plaintext, str) else plaintext
36+
)
37+
m_public_key = (
38+
b64decode(public_key) if isinstance(public_key, Base64String) else public_key
39+
)
40+
m_public_key = (
41+
public.PublicKey(m_public_key)
42+
if isinstance(m_public_key, (bytes, bytearray, memoryview))
43+
else m_public_key
44+
)
45+
m_private_key = (
46+
b64decode(private_key) if isinstance(private_key, Base64String) else private_key
47+
)
48+
m_private_key = (
49+
public.PrivateKey(m_private_key)
50+
if isinstance(m_private_key, (bytes, bytearray, memoryview))
51+
else m_private_key
52+
)
53+
54+
nonce = utils.random(24)
55+
box = public.Box(m_private_key, m_public_key)
56+
ciphertext = box.encrypt(m_plaintext, nonce).ciphertext
57+
58+
return (b64encode(ciphertext).decode("utf-8"), b64encode(nonce).decode("utf-8"))
59+
60+
61+
def decrypt_asymmetric(
62+
ciphertext: Union[Buffer, Base64String],
63+
nonce: Union[Buffer, Base64String],
64+
public_key: Union[Buffer, Base64String, public.PublicKey],
65+
private_key: Union[Buffer, Base64String, public.PrivateKey],
66+
) -> str:
67+
"""Performs asymmetric decryption of the ``ciphertext`` with x25519-xsalsa20-poly1305
68+
algorithm with the given parameters.
69+
Each of those params should be either the raw value in bytes or a base64 string.
70+
71+
:param ciphertext: The ciphered text to decrypt
72+
:param nonce: The nonce used for encryption
73+
:param public_key: The public key
74+
:param private_key: The private key
75+
76+
:raises ValueError: If ``ciphertext``, ``nonce``, ``public_key`` or ``private_key`` are empty
77+
78+
:return: The deciphered text
79+
"""
80+
if (
81+
len(ciphertext) == 0
82+
or len(nonce) == 0
83+
or (not isinstance(public_key, public.PublicKey) and len(public_key) == 0)
84+
or (not isinstance(private_key, public.PrivateKey) and len(private_key) == 0)
85+
):
86+
raise ValueError(
87+
"Public key, private key, ciphertext and nonce cannot be empty!"
88+
)
89+
90+
m_ciphertext = (
91+
b64decode(ciphertext) if isinstance(ciphertext, Base64String) else ciphertext
92+
)
93+
m_nonce = b64decode(nonce) if isinstance(nonce, Base64String) else nonce
94+
m_public_key = (
95+
b64decode(public_key) if isinstance(public_key, Base64String) else public_key
96+
)
97+
m_public_key = (
98+
public.PublicKey(m_public_key)
99+
if isinstance(m_public_key, (bytes, bytearray, memoryview))
100+
else m_public_key
101+
)
102+
m_private_key = (
103+
b64decode(private_key) if isinstance(private_key, Base64String) else private_key
104+
)
105+
m_private_key = (
106+
public.PrivateKey(m_private_key)
107+
if isinstance(m_private_key, (bytes, bytearray, memoryview))
108+
else m_private_key
109+
)
110+
111+
box = public.Box(m_private_key, m_public_key)
112+
plaintext = box.decrypt(m_ciphertext, m_nonce)
113+
114+
return plaintext.decode("utf-8")
115+
116+
117+
def create_symmetric_key_helper():
118+
return b64encode(get_random_bytes(32)).decode("utf-8")
119+
120+
121+
def encrypt_symmetric_helper(plaintext: str, key: str):
122+
iv = get_random_bytes(12)
123+
124+
cipher = AES.new(b64decode(key), AES.MODE_GCM, nonce=iv)
125+
126+
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode("utf-8"))
127+
128+
return (
129+
b64encode(ciphertext).decode("utf-8"),
130+
b64encode(iv).decode("utf-8"),
131+
b64encode(tag).decode("utf-8"),
132+
)
133+
134+
135+
def decrypt_symmetric_helper(ciphertext: str, key: str, iv: str, tag: str):
136+
cipher = AES.new(b64decode(key), AES.MODE_GCM, nonce=b64decode(iv))
137+
plaintext = cipher.decrypt_and_verify(b64decode(ciphertext), b64decode(tag))
138+
139+
return plaintext.decode("utf-8")
140+
141+
142+
def encrypt_symmetric_128_bit_hex_key_utf8(
143+
plaintext: str, key: str
144+
) -> Tuple[Base64String, Base64String, Base64String]:
145+
"""Encrypts the ``plaintext`` with aes-256-gcm using the given ``key``.
146+
The key should be either the raw value in bytes or a base64 string.
147+
148+
:param plaintext: text to encrypt
149+
:param key: UTF-8, 128-bit AES key used for encryption
150+
151+
:raises ValueError: If either ``plaintext`` or ``key`` is empty
152+
153+
:return: Ciphered text
154+
"""
155+
if len(key) == 0:
156+
raise ValueError("The given key is empty!")
157+
158+
BLOCK_SIZE_BYTES = 16
159+
160+
iv = get_random_bytes(BLOCK_SIZE_BYTES)
161+
cipher = AES.new(bytes(key, "utf-8"), AES.MODE_GCM, nonce=iv)
162+
163+
ciphertext, tag = cipher.encrypt_and_digest(str.encode(plaintext, "utf-8"))
164+
165+
return (
166+
b64encode(ciphertext).decode("utf-8"),
167+
b64encode(iv).decode("utf-8"),
168+
b64encode(tag).decode("utf-8"),
169+
)
170+
171+
172+
def decrypt_symmetric_128_bit_hex_key_utf8(
173+
key: str, ciphertext: str, tag: str, iv: str
174+
) -> str:
175+
"""Decrypts the ``ciphertext`` with aes-256-gcm using ``iv``, ``tag``
176+
and ``key``.
177+
178+
:param key: UTF-8, 128-bit hex AES key
179+
:param ciphertext: base64 ciphered text to decrypt
180+
:param tag: base64 tag/mac used for verification
181+
:param iv: base64 nonce
182+
183+
:raises ValueError:
184+
If ``ciphertext``, ``iv``, ``tag`` or ``key`` are empty or tag/mac doesn't match
185+
186+
:return: Deciphered text
187+
"""
188+
if len(tag) == 0 or len(iv) == 0 or len(key) == 0:
189+
raise ValueError("One of the given parameter is empty!")
190+
191+
try:
192+
key = bytes(key, "utf-8")
193+
iv = b64decode(iv)
194+
tag = b64decode(tag)
195+
ciphertext = b64decode(ciphertext)
196+
197+
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
198+
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
199+
200+
return plaintext.decode("utf-8")
201+
except ValueError:
202+
raise ValueError("Incorrect decryption or MAC check failed")

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ aenum >= 3.1.11
55
requests >= 2.31.0
66
boto3 >= 1.33.8
77
botocore >= 1.33.8
8+
pycryptodomex >= 3.20.0
9+
PyNaCl >= 1.5.0

0 commit comments

Comments
 (0)