securepayload 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- securepayload/__init__.py +124 -0
- securepayload/aes.py +170 -0
- securepayload/encryption_service.py +59 -0
- securepayload/env.py +56 -0
- securepayload/exceptions.py +14 -0
- securepayload/vectors.py +9 -0
- securepayload-1.0.0.dist-info/METADATA +309 -0
- securepayload-1.0.0.dist-info/RECORD +10 -0
- securepayload-1.0.0.dist-info/WHEEL +5 -0
- securepayload-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecurePayload — Python AES API payload encryption.
|
|
3
|
+
|
|
4
|
+
Primary usage::
|
|
5
|
+
|
|
6
|
+
import securepayload
|
|
7
|
+
|
|
8
|
+
securepayload.bootstrap()
|
|
9
|
+
ciphertext = securepayload.encrypt({"order_no": "m123"})
|
|
10
|
+
payload = securepayload.decrypt(ciphertext)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .aes import Aes
|
|
20
|
+
from .encryption_service import EncryptionService
|
|
21
|
+
from .env import find_env_file, load_env
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
DecryptionError,
|
|
24
|
+
EncryptionError,
|
|
25
|
+
InvalidKeyError,
|
|
26
|
+
SecurityEncryptionError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__version__ = "1.0.0"
|
|
30
|
+
|
|
31
|
+
_service: EncryptionService | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def configure(
|
|
35
|
+
key: str | None = None,
|
|
36
|
+
*,
|
|
37
|
+
key_size: int = 128,
|
|
38
|
+
iteration_count: int = 8,
|
|
39
|
+
iv: str = "",
|
|
40
|
+
env_file: Path | str | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Set the AES key used by ``encrypt`` and ``decrypt``.
|
|
44
|
+
|
|
45
|
+
If ``key`` is omitted, loads ``SECURITY_AES_KEY`` from the environment
|
|
46
|
+
(searching for ``.env`` when needed).
|
|
47
|
+
"""
|
|
48
|
+
global _service
|
|
49
|
+
|
|
50
|
+
resolved = key
|
|
51
|
+
if not resolved:
|
|
52
|
+
load_env(env_file)
|
|
53
|
+
resolved = os.environ.get("SECURITY_AES_KEY")
|
|
54
|
+
|
|
55
|
+
if not resolved:
|
|
56
|
+
raise InvalidKeyError(
|
|
57
|
+
"AES key is not configured. Set SECURITY_AES_KEY in .env or call configure(key=...)."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_service = EncryptionService(
|
|
61
|
+
resolved,
|
|
62
|
+
key_size=key_size,
|
|
63
|
+
iteration_count=iteration_count,
|
|
64
|
+
iv=iv,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def bootstrap(
|
|
69
|
+
env_file: Path | str | None = None,
|
|
70
|
+
*,
|
|
71
|
+
key: str | None = None,
|
|
72
|
+
key_size: int = 128,
|
|
73
|
+
iteration_count: int = 8,
|
|
74
|
+
iv: str = "",
|
|
75
|
+
) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Load ``.env`` and configure the module in one step.
|
|
78
|
+
|
|
79
|
+
If ``env_file`` is omitted, searches upward from the current working directory
|
|
80
|
+
for a ``.env`` file. Returns the configured key.
|
|
81
|
+
"""
|
|
82
|
+
if key is None:
|
|
83
|
+
load_env(env_file)
|
|
84
|
+
key = os.environ.get("SECURITY_AES_KEY")
|
|
85
|
+
if not key:
|
|
86
|
+
raise InvalidKeyError(
|
|
87
|
+
"AES key is not configured. Set SECURITY_AES_KEY in .env or call bootstrap(key=...)."
|
|
88
|
+
)
|
|
89
|
+
configure(key=key, key_size=key_size, iteration_count=iteration_count, iv=iv)
|
|
90
|
+
return key
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_service() -> EncryptionService:
|
|
94
|
+
if _service is None:
|
|
95
|
+
configure()
|
|
96
|
+
assert _service is not None
|
|
97
|
+
return _service
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def encrypt(data: dict | list | str) -> str:
|
|
101
|
+
"""Encrypt a payload to a Base64 AES ciphertext string."""
|
|
102
|
+
return _get_service().encrypt(data)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def decrypt(encrypted_data: Any) -> Any:
|
|
106
|
+
"""Decrypt a Base64 ciphertext string or pass through dict/list payloads."""
|
|
107
|
+
return _get_service().decrypt(encrypted_data)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"Aes",
|
|
112
|
+
"EncryptionService",
|
|
113
|
+
"SecurityEncryptionError",
|
|
114
|
+
"EncryptionError",
|
|
115
|
+
"DecryptionError",
|
|
116
|
+
"InvalidKeyError",
|
|
117
|
+
"configure",
|
|
118
|
+
"bootstrap",
|
|
119
|
+
"encrypt",
|
|
120
|
+
"decrypt",
|
|
121
|
+
"load_env",
|
|
122
|
+
"find_env_file",
|
|
123
|
+
"__version__",
|
|
124
|
+
]
|
securepayload/aes.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Callable, Iterable, Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
from Crypto.Cipher import AES
|
|
10
|
+
|
|
11
|
+
from .exceptions import DecryptionError, EncryptionError, InvalidKeyError, SecurityEncryptionError
|
|
12
|
+
|
|
13
|
+
_JSON_SEPARATORS = (",", ":")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prepare_key(key: str, key_size: int = 128) -> bytes:
|
|
17
|
+
if not key:
|
|
18
|
+
raise InvalidKeyError("AES key must be a non-empty string.")
|
|
19
|
+
|
|
20
|
+
byte_len = key_size // 8
|
|
21
|
+
raw = key.encode("utf-8")
|
|
22
|
+
|
|
23
|
+
if len(raw) < byte_len:
|
|
24
|
+
return raw.ljust(byte_len, b"\0")
|
|
25
|
+
if len(raw) > byte_len:
|
|
26
|
+
return raw[:byte_len]
|
|
27
|
+
return raw
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
|
|
31
|
+
pad_len = block_size - (len(data) % block_size)
|
|
32
|
+
return data + bytes([pad_len] * pad_len)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
|
|
36
|
+
if not data:
|
|
37
|
+
raise DecryptionError("Cannot unpad empty ciphertext.")
|
|
38
|
+
pad_len = data[-1]
|
|
39
|
+
if pad_len < 1 or pad_len > block_size:
|
|
40
|
+
raise DecryptionError("Invalid PKCS#7 padding.")
|
|
41
|
+
if data[-pad_len:] != bytes([pad_len] * pad_len):
|
|
42
|
+
raise DecryptionError("Invalid PKCS#7 padding bytes.")
|
|
43
|
+
return data[:-pad_len]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _to_json(data: Any) -> str:
|
|
47
|
+
return json.dumps(data, separators=_JSON_SEPARATORS, ensure_ascii=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Aes:
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
key: str,
|
|
54
|
+
*,
|
|
55
|
+
key_size: int = 128,
|
|
56
|
+
iteration_count: int = 8,
|
|
57
|
+
iv: str = "",
|
|
58
|
+
) -> None:
|
|
59
|
+
# iteration_count and iv are accepted for API compatibility; ECB ignores IV.
|
|
60
|
+
self.key_size = key_size
|
|
61
|
+
self.iteration_count = iteration_count
|
|
62
|
+
self.iv = iv
|
|
63
|
+
self.key = key
|
|
64
|
+
self._key_bytes = prepare_key(key, key_size)
|
|
65
|
+
|
|
66
|
+
def set_new_key(self, iv: str, key: str) -> tuple[str, str]:
|
|
67
|
+
"""Update IV and key. Empty IV is allowed (ECB mode does not use IV)."""
|
|
68
|
+
if not key:
|
|
69
|
+
raise InvalidKeyError("Invalid key")
|
|
70
|
+
self.iv = iv
|
|
71
|
+
self.key = key
|
|
72
|
+
self._key_bytes = prepare_key(key, self.key_size)
|
|
73
|
+
return self.iv, self.key
|
|
74
|
+
|
|
75
|
+
def obj_copy(self, obj: Any) -> Any:
|
|
76
|
+
"""Deep copy helper used by ``obj_pipe``."""
|
|
77
|
+
return copy.deepcopy(obj)
|
|
78
|
+
|
|
79
|
+
def obj_pipe(
|
|
80
|
+
self,
|
|
81
|
+
obj: Mapping[str, Any],
|
|
82
|
+
mode: int,
|
|
83
|
+
props: Iterable[str],
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""Encrypt (mode > 0) or decrypt (mode <= 0) selected object properties."""
|
|
86
|
+
result = dict(self.obj_copy(obj))
|
|
87
|
+
for prop in props:
|
|
88
|
+
if prop not in obj:
|
|
89
|
+
continue
|
|
90
|
+
result[prop] = self.encrypt(obj[prop]) if mode > 0 else self.decrypt(obj[prop])
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def _transform(
|
|
94
|
+
self,
|
|
95
|
+
data: Any,
|
|
96
|
+
*,
|
|
97
|
+
props: Sequence[str] | None,
|
|
98
|
+
pipe_mode: int,
|
|
99
|
+
on_scalar: Callable[[str], str],
|
|
100
|
+
error_cls: type[SecurityEncryptionError],
|
|
101
|
+
action: str,
|
|
102
|
+
) -> Any:
|
|
103
|
+
if isinstance(data, Mapping):
|
|
104
|
+
if props:
|
|
105
|
+
return self.obj_pipe(data, pipe_mode, props)
|
|
106
|
+
return on_scalar(_to_json(data))
|
|
107
|
+
if isinstance(data, (list, tuple)):
|
|
108
|
+
if props:
|
|
109
|
+
return [self.obj_pipe(item, pipe_mode, props) for item in data]
|
|
110
|
+
return on_scalar(_to_json(data))
|
|
111
|
+
if isinstance(data, str):
|
|
112
|
+
return on_scalar(data)
|
|
113
|
+
if isinstance(data, int):
|
|
114
|
+
return on_scalar(str(data))
|
|
115
|
+
raise error_cls(f"Unsupported {action} input type: {type(data).__name__}")
|
|
116
|
+
|
|
117
|
+
def encrypt(self, data: Any, props: Sequence[str] | None = None) -> Any:
|
|
118
|
+
"""Encrypt strings, numbers, dicts, or lists."""
|
|
119
|
+
return self._transform(
|
|
120
|
+
data,
|
|
121
|
+
props=props,
|
|
122
|
+
pipe_mode=1,
|
|
123
|
+
on_scalar=self.do_encrypt,
|
|
124
|
+
error_cls=EncryptionError,
|
|
125
|
+
action="encrypt",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def decrypt(self, data: Any, props: Sequence[str] | None = None) -> Any:
|
|
129
|
+
"""Decrypt strings, numbers, dicts, or lists."""
|
|
130
|
+
return self._transform(
|
|
131
|
+
data,
|
|
132
|
+
props=props,
|
|
133
|
+
pipe_mode=0,
|
|
134
|
+
on_scalar=self.do_decrypt,
|
|
135
|
+
error_cls=DecryptionError,
|
|
136
|
+
action="decrypt",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def do_encrypt(self, plain_text: str) -> str:
|
|
140
|
+
"""Encrypt plaintext and return Base64 ciphertext."""
|
|
141
|
+
try:
|
|
142
|
+
cipher = AES.new(self._key_bytes, AES.MODE_ECB)
|
|
143
|
+
padded = _pkcs7_pad(plain_text.encode("utf-8"))
|
|
144
|
+
encrypted = cipher.encrypt(padded)
|
|
145
|
+
return base64.b64encode(encrypted).decode("ascii")
|
|
146
|
+
except SecurityEncryptionError:
|
|
147
|
+
raise
|
|
148
|
+
except (TypeError, ValueError, UnicodeError) as exc:
|
|
149
|
+
raise EncryptionError("Encryption failed.") from exc
|
|
150
|
+
|
|
151
|
+
def do_decrypt(self, cipher_text: str, key: str | None = None, iv: str | None = None) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Decrypt Base64 ciphertext.
|
|
154
|
+
|
|
155
|
+
``key`` and ``iv`` are optional overrides; only ``key`` is applied when
|
|
156
|
+
provided. ECB mode ignores IV.
|
|
157
|
+
"""
|
|
158
|
+
_ = iv # ECB mode does not use IV.
|
|
159
|
+
try:
|
|
160
|
+
key_bytes = self._key_bytes if key is None else prepare_key(key, self.key_size)
|
|
161
|
+
raw = base64.b64decode(cipher_text, validate=True)
|
|
162
|
+
if len(raw) % 16 != 0:
|
|
163
|
+
raise DecryptionError("Ciphertext length must be a multiple of the AES block size.")
|
|
164
|
+
cipher = AES.new(key_bytes, AES.MODE_ECB)
|
|
165
|
+
decrypted = cipher.decrypt(raw)
|
|
166
|
+
return _pkcs7_unpad(decrypted).decode("utf-8")
|
|
167
|
+
except SecurityEncryptionError:
|
|
168
|
+
raise
|
|
169
|
+
except (binascii.Error, TypeError, ValueError, UnicodeError) as exc:
|
|
170
|
+
raise DecryptionError("Decryption failed.") from exc
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""High-level encryption service for API payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .aes import Aes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EncryptionService:
|
|
12
|
+
"""
|
|
13
|
+
Encrypt and decrypt API payloads using AES-128-ECB.
|
|
14
|
+
|
|
15
|
+
Provide your own AES key (16 characters for AES-128) to encrypt/decrypt
|
|
16
|
+
payloads exchanged with APIs.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
key: str,
|
|
22
|
+
*,
|
|
23
|
+
key_size: int = 128,
|
|
24
|
+
iteration_count: int = 8,
|
|
25
|
+
iv: str = "",
|
|
26
|
+
) -> None:
|
|
27
|
+
self._aes = Aes(
|
|
28
|
+
key,
|
|
29
|
+
key_size=key_size,
|
|
30
|
+
iteration_count=iteration_count,
|
|
31
|
+
iv=iv,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def decrypt(self, encrypted_data: Any) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Decrypt AES payload.
|
|
37
|
+
|
|
38
|
+
- dict/list: returned unchanged (already decoded)
|
|
39
|
+
- str: decrypted and JSON-decoded when possible; raw string otherwise
|
|
40
|
+
- other: ``None``
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(encrypted_data, (dict, list)):
|
|
43
|
+
return encrypted_data
|
|
44
|
+
|
|
45
|
+
if isinstance(encrypted_data, str):
|
|
46
|
+
plaintext = self._aes.decrypt(encrypted_data)
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(plaintext)
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
return plaintext
|
|
51
|
+
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def encrypt(self, data: dict | list | str) -> str:
|
|
55
|
+
"""Encrypt data to a Base64 AES string."""
|
|
56
|
+
return self._aes.encrypt(data)
|
|
57
|
+
|
|
58
|
+
Decrypt = decrypt
|
|
59
|
+
Encrypt = encrypt
|
securepayload/env.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Load environment variables from a .env file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_env_file(start: Path | None = None) -> Path | None:
|
|
10
|
+
"""Walk upward from ``start`` (default: cwd) looking for a ``.env`` file."""
|
|
11
|
+
current = (start or Path.cwd()).resolve()
|
|
12
|
+
for directory in (current, *current.parents):
|
|
13
|
+
candidate = directory / ".env"
|
|
14
|
+
if candidate.is_file():
|
|
15
|
+
return candidate
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_env(env_file: Path | str | None = None) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Load variables from a ``.env`` file into ``os.environ``.
|
|
22
|
+
|
|
23
|
+
Uses python-dotenv when installed; otherwise parses the file directly.
|
|
24
|
+
When ``env_file`` is omitted, searches upward from the current working directory.
|
|
25
|
+
Returns ``True`` if a file was found and loaded.
|
|
26
|
+
"""
|
|
27
|
+
if env_file is not None:
|
|
28
|
+
path = Path(env_file)
|
|
29
|
+
else:
|
|
30
|
+
found = find_env_file()
|
|
31
|
+
if found is None:
|
|
32
|
+
return False
|
|
33
|
+
path = found
|
|
34
|
+
|
|
35
|
+
if not path.is_file():
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from dotenv import load_dotenv
|
|
40
|
+
|
|
41
|
+
load_dotenv(path)
|
|
42
|
+
return True
|
|
43
|
+
except ImportError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
47
|
+
stripped = line.strip()
|
|
48
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
49
|
+
continue
|
|
50
|
+
key, _, value = stripped.partition("=")
|
|
51
|
+
key = key.strip()
|
|
52
|
+
value = value.strip().strip('"').strip("'")
|
|
53
|
+
if key:
|
|
54
|
+
os.environ.setdefault(key, value)
|
|
55
|
+
|
|
56
|
+
return True
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class SecurityEncryptionError(Exception):
|
|
2
|
+
"""Base exception for encryption package errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InvalidKeyError(SecurityEncryptionError):
|
|
6
|
+
"""Raised when the AES key is missing or invalid."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EncryptionError(SecurityEncryptionError):
|
|
10
|
+
"""Raised when encryption fails."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DecryptionError(SecurityEncryptionError):
|
|
14
|
+
"""Raised when decryption fails."""
|
securepayload/vectors.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Shared known ciphertext vectors for tests and examples."""
|
|
2
|
+
|
|
3
|
+
KNOWN_STRING_PLAIN = {"test": "hello"}
|
|
4
|
+
KNOWN_STRING_CIPHER = "4f58KzglCzu10lH/7VxEy+tBHZ/TaMAkHQSH/SnDBEI="
|
|
5
|
+
|
|
6
|
+
KNOWN_ORDER_PLAIN = {"order_no": "m123"}
|
|
7
|
+
KNOWN_ORDER_CIPHER = "3zBcPh7pTI8VHlNt6MdfQxHTv+BOkN5Gg5gmBxzQ07g="
|
|
8
|
+
|
|
9
|
+
SAMPLE_KEY = "abcdefghijklmnop"
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: securepayload
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SecurePayload — Python AES encryption library for API payloads. Encrypt and decrypt with securepayload.encrypt() and securepayload.decrypt().
|
|
5
|
+
Author-email: zfhassaan <zfhassaan@gmail.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/zfhassaan/securepayload
|
|
8
|
+
Project-URL: Documentation, https://github.com/zfhassaan/securepayload#readme
|
|
9
|
+
Project-URL: Source, https://github.com/zfhassaan/securepayload
|
|
10
|
+
Project-URL: Issues, https://github.com/zfhassaan/securepayload/issues
|
|
11
|
+
Keywords: aes,aes-encryption,api-payload,cryptography,encryption,payload-encryption,python-security,secure-payload,webhook-encryption
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: pycryptodome>=3.20.0
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="assets/banner.png" alt="SecurePayload — Python AES API Payload Encryption Library" width="100%" />
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="assets/logo.png" alt="SecurePayload logo" width="50" />
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<h1 align="center">SecurePayload</h1>
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<strong>Python AES encryption library for secure API payload handling</strong>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
Encrypt and decrypt JSON API payloads, webhook bodies, and request data with a simple two-call API —
|
|
44
|
+
<code>securepayload.encrypt()</code> and <code>securepayload.decrypt()</code>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<a href="#installation">Installation</a> •
|
|
49
|
+
<a href="#quick-start">Quick Start</a> •
|
|
50
|
+
<a href="#usage">Usage</a> •
|
|
51
|
+
<a href="#api-reference">API Reference</a> •
|
|
52
|
+
<a href="#testing">Testing</a>
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<img src="https://img.shields.io/badge/python-3.9+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.9+" />
|
|
57
|
+
<img src="https://img.shields.io/badge/AES--128--ECB-14b8a6?style=flat-square" alt="AES-128-ECB" />
|
|
58
|
+
<img src="https://img.shields.io/badge/license-Proprietary-64748b?style=flat-square" alt="License" />
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Overview
|
|
64
|
+
|
|
65
|
+
**SecurePayload** is a lightweight Python cryptography library built for developers who need to **encrypt API request bodies** and **decrypt encrypted webhook or HTTP payloads**. It uses AES-128-ECB with PKCS#7 padding and Base64 output — compatible with existing encrypted API integrations.
|
|
66
|
+
|
|
67
|
+
Ideal for:
|
|
68
|
+
|
|
69
|
+
- Python microservices sending encrypted API requests
|
|
70
|
+
- Webhook receivers decrypting incoming payloads
|
|
71
|
+
- Background workers and ETL pipelines handling secure JSON data
|
|
72
|
+
- Integration scripts bridging encrypted API endpoints
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import securepayload
|
|
76
|
+
|
|
77
|
+
securepayload.bootstrap()
|
|
78
|
+
|
|
79
|
+
encrypted = securepayload.encrypt({"order_no": "m123", "channel": "CARD"})
|
|
80
|
+
decrypted = securepayload.decrypt(encrypted)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Features
|
|
86
|
+
|
|
87
|
+
| Feature | Description |
|
|
88
|
+
|---------|-------------|
|
|
89
|
+
| **Simple API** | `securepayload.encrypt()` and `securepayload.decrypt()` — minimal boilerplate |
|
|
90
|
+
| **API payload ready** | JSON dicts and lists encrypted automatically |
|
|
91
|
+
| **Environment-based keys** | Configure via `securepayload.bootstrap()` or `configure(key=...)` |
|
|
92
|
+
| **Selective field encryption** | `Aes.obj_pipe()` for encrypting individual record fields |
|
|
93
|
+
| **Typed exceptions** | `InvalidKeyError`, `EncryptionError`, `DecryptionError` |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- Python **3.9+**
|
|
100
|
+
- [PyCryptodome](https://pycryptodome.readthedocs.io/)
|
|
101
|
+
- [python-dotenv](https://github.com/theskumar/python-dotenv)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
### From PyPI
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install securepayload
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### From source (development)
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/zfhassaan/securepayload.git
|
|
117
|
+
cd securepayload
|
|
118
|
+
python -m venv env
|
|
119
|
+
|
|
120
|
+
# Windows
|
|
121
|
+
env\Scripts\activate
|
|
122
|
+
|
|
123
|
+
# macOS / Linux
|
|
124
|
+
source env/bin/activate
|
|
125
|
+
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Configure your AES key
|
|
130
|
+
|
|
131
|
+
Create a `.env` file (or set the variable in your environment):
|
|
132
|
+
|
|
133
|
+
```env
|
|
134
|
+
SECURITY_AES_KEY=your-16-char-key
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> **Security:** Never commit production keys. Keep `.env` out of version control.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Quick start
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import securepayload
|
|
145
|
+
|
|
146
|
+
securepayload.bootstrap() # loads .env and configures the key
|
|
147
|
+
|
|
148
|
+
payload = {"order_no": "26168785012837", "channel": "CARD"}
|
|
149
|
+
|
|
150
|
+
encrypted = securepayload.encrypt(payload)
|
|
151
|
+
decrypted = securepayload.decrypt(encrypted)
|
|
152
|
+
|
|
153
|
+
print(encrypted) # Base64 ciphertext
|
|
154
|
+
print(decrypted) # Original dict
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python examples/basic_usage.py
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Usage
|
|
164
|
+
|
|
165
|
+
### Encrypt and decrypt API payloads
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import securepayload
|
|
169
|
+
|
|
170
|
+
# Load key from .env (searches upward from cwd)
|
|
171
|
+
securepayload.bootstrap()
|
|
172
|
+
|
|
173
|
+
# Or pass key explicitly
|
|
174
|
+
securepayload.configure(key="your-16-char-key")
|
|
175
|
+
|
|
176
|
+
# Encrypt JSON payload → Base64 string
|
|
177
|
+
ciphertext = securepayload.encrypt({"order_no": "m123", "channel": "CARD"})
|
|
178
|
+
|
|
179
|
+
# Decrypt → dict (auto JSON-parsed)
|
|
180
|
+
data = securepayload.decrypt(ciphertext)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Input to `decrypt()` | Result |
|
|
184
|
+
|----------------------|--------|
|
|
185
|
+
| Base64 `str` | Decrypted; JSON-parsed when valid JSON |
|
|
186
|
+
| `dict` / `list` | Returned unchanged |
|
|
187
|
+
| Other | `None` |
|
|
188
|
+
|
|
189
|
+
### HTTP integration
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
import os
|
|
193
|
+
import requests
|
|
194
|
+
import securepayload
|
|
195
|
+
|
|
196
|
+
securepayload.configure(key=os.environ["SECURITY_AES_KEY"])
|
|
197
|
+
|
|
198
|
+
body = securepayload.encrypt({"event": "order.updated", "order_no": "m123"})
|
|
199
|
+
requests.post("https://api.example.com/webhook", data=body)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Error handling
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
import securepayload
|
|
206
|
+
from securepayload.exceptions import DecryptionError, InvalidKeyError
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
securepayload.configure(key="")
|
|
210
|
+
except InvalidKeyError:
|
|
211
|
+
...
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
securepayload.decrypt("invalid-ciphertext")
|
|
215
|
+
except DecryptionError:
|
|
216
|
+
...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Advanced: selective field encryption
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from securepayload import Aes
|
|
223
|
+
|
|
224
|
+
aes = Aes(key="your-16-char-key")
|
|
225
|
+
record = {"name": "public", "token": "secret-value"}
|
|
226
|
+
sealed = aes.obj_pipe(record, mode=1, props=["token"])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Examples
|
|
232
|
+
|
|
233
|
+
| Script | Description |
|
|
234
|
+
|--------|-------------|
|
|
235
|
+
| `examples/basic_usage.py` | Encrypt/decrypt demo |
|
|
236
|
+
| `examples/vector_test.py` | Known ciphertext vector validation |
|
|
237
|
+
| `examples/run_tests.py` | Runs the pytest suite |
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
python examples/basic_usage.py
|
|
241
|
+
python examples/vector_test.py
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Testing
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
pip install -e ".[dev]"
|
|
250
|
+
pytest -v
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Cryptographic specification
|
|
256
|
+
|
|
257
|
+
| Setting | Value |
|
|
258
|
+
|---------|-------|
|
|
259
|
+
| Algorithm | AES-128-ECB |
|
|
260
|
+
| Padding | PKCS#7 |
|
|
261
|
+
| Output | Base64 |
|
|
262
|
+
| Key | 16-byte UTF-8 string (padded/truncated) |
|
|
263
|
+
|
|
264
|
+
### Known test vectors
|
|
265
|
+
|
|
266
|
+
| Plaintext | Ciphertext |
|
|
267
|
+
|-----------|------------|
|
|
268
|
+
| `{"test":"hello"}` | `4f58KzglCzu10lH/7VxEy+tBHZ/TaMAkHQSH/SnDBEI=` |
|
|
269
|
+
| `{"order_no":"m123"}` | `3zBcPh7pTI8VHlNt6MdfQxHTv+BOkN5Gg5gmBxzQ07g=` |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Project structure
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
├── assets/
|
|
277
|
+
│ ├── banner.png # README banner
|
|
278
|
+
│ └── logo.png # Project logo
|
|
279
|
+
├── securepayload/ # Main package
|
|
280
|
+
│ ├── __init__.py # securepayload.encrypt / decrypt
|
|
281
|
+
│ ├── aes.py
|
|
282
|
+
│ ├── encryption_service.py
|
|
283
|
+
│ └── exceptions.py
|
|
284
|
+
├── examples/
|
|
285
|
+
├── tests/
|
|
286
|
+
└── docs/
|
|
287
|
+
└── API.md
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Security considerations
|
|
293
|
+
|
|
294
|
+
- **ECB mode** is retained for compatibility with existing encrypted API systems.
|
|
295
|
+
- Use `.env` locally and a secrets manager in production.
|
|
296
|
+
- Rotate keys through your deployment pipeline.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Documentation
|
|
301
|
+
|
|
302
|
+
- [API reference](docs/API.md)
|
|
303
|
+
- Repository: [github.com/zfhassaan/securepayload](https://github.com/zfhassaan/securepayload)
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
Proprietary — internal tooling. Use according to your organization's policies.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
securepayload/__init__.py,sha256=OeB0UOLhmy9KR7GK48oufMs8b7yXL_pe9YXaSr7VZfo,2918
|
|
2
|
+
securepayload/aes.py,sha256=hJha9JxEfauiFUTjDvp5CxM23z6WiRzcb9OyGM5eHPc,5788
|
|
3
|
+
securepayload/encryption_service.py,sha256=9lJG39UXZocGwMqwnWfYKKbhJfR0coPk5OAl3FIjlqo,1473
|
|
4
|
+
securepayload/env.py,sha256=Qwt9TWjmov696f7I4VpDd5Eb8Y8ngI3NNoQlYPosb8U,1584
|
|
5
|
+
securepayload/exceptions.py,sha256=ASrQzgzEClFwDerzJfx8hSoGau3giY2ZG4te_8XEWkY,385
|
|
6
|
+
securepayload/vectors.py,sha256=Zg_w50lcWHjCgB5XgzBOBL2aFS52TMBVZT5fV1QTyP8,314
|
|
7
|
+
securepayload-1.0.0.dist-info/METADATA,sha256=wl6QgBPZZqiTrq_n9WR4UMt8iAibg5Cnr4kKuTC0XOE,7996
|
|
8
|
+
securepayload-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
securepayload-1.0.0.dist-info/top_level.txt,sha256=6q4BgFB9aqOpCkzymWB7qY-mdEPEr51VF2xuX2suCE8,14
|
|
10
|
+
securepayload-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
securepayload
|