panda-crypto-py 0.1.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.
- panda/__init__.py +62 -0
- panda/audit/__init__.py +23 -0
- panda/audit/logger.py +160 -0
- panda/blind_index/__init__.py +11 -0
- panda/blind_index/blind_index.py +145 -0
- panda/cli/__init__.py +7 -0
- panda/cli/commands.py +513 -0
- panda/crypto/__init__.py +54 -0
- panda/crypto/cipher.py +239 -0
- panda/crypto/envelope.py +125 -0
- panda/crypto/errors.py +50 -0
- panda/crypto/keys.py +655 -0
- panda/integrations/__init__.py +7 -0
- panda/integrations/django.py +183 -0
- panda/integrations/fastapi.py +205 -0
- panda/integrations/flask.py +140 -0
- panda/masking/__init__.py +12 -0
- panda/masking/masker.py +73 -0
- panda/storage/__init__.py +13 -0
- panda/storage/storage.py +86 -0
- panda/telemetry/__init__.py +11 -0
- panda/telemetry/telemetry.py +144 -0
- panda_crypto_py-0.1.0.dist-info/METADATA +283 -0
- panda_crypto_py-0.1.0.dist-info/RECORD +28 -0
- panda_crypto_py-0.1.0.dist-info/WHEEL +5 -0
- panda_crypto_py-0.1.0.dist-info/entry_points.txt +2 -0
- panda_crypto_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- panda_crypto_py-0.1.0.dist-info/top_level.txt +1 -0
panda/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PANDA - Private And Secure Data Access SDK for Python
|
|
3
|
+
|
|
4
|
+
Field-level encryption and masking for sensitive personal data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from .crypto import (
|
|
10
|
+
FieldCipher,
|
|
11
|
+
Envelope,
|
|
12
|
+
LocalKeyProvider,
|
|
13
|
+
VaultKeyProvider,
|
|
14
|
+
InfisicalKeyProvider,
|
|
15
|
+
create_key_provider,
|
|
16
|
+
KeyNotFoundError,
|
|
17
|
+
AuthenticationFailedError,
|
|
18
|
+
EncryptionError
|
|
19
|
+
)
|
|
20
|
+
from .masking import Masker, MaskingConfig
|
|
21
|
+
from .blind_index import BlindIndexer
|
|
22
|
+
from .audit import AuditLogger, ConsoleAuditLogger, NullAuditLogger
|
|
23
|
+
from .telemetry import TelemetryClient
|
|
24
|
+
from .storage import TLSVerifier, MySqlAdapter, PostgresAdapter
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Version
|
|
28
|
+
"__version__",
|
|
29
|
+
|
|
30
|
+
# Core crypto
|
|
31
|
+
"FieldCipher",
|
|
32
|
+
"Envelope",
|
|
33
|
+
"LocalKeyProvider",
|
|
34
|
+
"VaultKeyProvider",
|
|
35
|
+
"InfisicalKeyProvider",
|
|
36
|
+
"create_key_provider",
|
|
37
|
+
|
|
38
|
+
# Errors
|
|
39
|
+
"KeyNotFoundError",
|
|
40
|
+
"AuthenticationFailedError",
|
|
41
|
+
"EncryptionError",
|
|
42
|
+
|
|
43
|
+
# Masking
|
|
44
|
+
"Masker",
|
|
45
|
+
"MaskingConfig",
|
|
46
|
+
|
|
47
|
+
# Blind Index
|
|
48
|
+
"BlindIndexer",
|
|
49
|
+
|
|
50
|
+
# Audit
|
|
51
|
+
"AuditLogger",
|
|
52
|
+
"ConsoleAuditLogger",
|
|
53
|
+
"NullAuditLogger",
|
|
54
|
+
|
|
55
|
+
# Telemetry
|
|
56
|
+
"TelemetryClient",
|
|
57
|
+
|
|
58
|
+
# Storage
|
|
59
|
+
"TLSVerifier",
|
|
60
|
+
"MySqlAdapter",
|
|
61
|
+
"PostgresAdapter",
|
|
62
|
+
]
|
panda/audit/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit logging module for PANDA SDK.
|
|
3
|
+
|
|
4
|
+
Provides structured logging of encryption/decryption events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .logger import (
|
|
8
|
+
AuditLogger,
|
|
9
|
+
ConsoleAuditLogger,
|
|
10
|
+
FileAuditLogger,
|
|
11
|
+
NullAuditLogger,
|
|
12
|
+
EventType,
|
|
13
|
+
Outcome
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AuditLogger",
|
|
18
|
+
"ConsoleAuditLogger",
|
|
19
|
+
"FileAuditLogger",
|
|
20
|
+
"NullAuditLogger",
|
|
21
|
+
"EventType",
|
|
22
|
+
"Outcome",
|
|
23
|
+
]
|
panda/audit/logger.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit logging module for PANDA SDK.
|
|
3
|
+
|
|
4
|
+
Provides structured logging of encryption/decryption events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventType(Enum):
|
|
15
|
+
"""Types of audit events."""
|
|
16
|
+
ENCRYPT = "encrypt"
|
|
17
|
+
DECRYPT_MASKED = "decrypt_masked"
|
|
18
|
+
DECRYPT_RAW = "decrypt_raw"
|
|
19
|
+
ROTATE = "rotate"
|
|
20
|
+
SHRED = "shred"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Outcome(Enum):
|
|
24
|
+
"""Possible outcomes of an operation."""
|
|
25
|
+
SUCCESS = "success"
|
|
26
|
+
AUTH_FAILURE = "auth_failure"
|
|
27
|
+
MALFORMED_ENVELOPE = "malformed_envelope"
|
|
28
|
+
KEY_NOT_FOUND = "key_not_found"
|
|
29
|
+
ENCRYPTION_FAILED = "encryption_failed"
|
|
30
|
+
UNSUPPORTED_ENVELOPE_VERSION = "unsupported_envelope_version"
|
|
31
|
+
UNSUPPORTED_ALGORITHM = "unsupported_algorithm"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuditLogger(ABC):
|
|
35
|
+
"""
|
|
36
|
+
Abstract base class for audit loggers.
|
|
37
|
+
|
|
38
|
+
Audit loggers record encryption/decryption events for compliance
|
|
39
|
+
and security monitoring. All decrypt_raw() calls (full plaintext
|
|
40
|
+
exposure) are logged distinctly from masked decrypt operations.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def log(
|
|
45
|
+
self,
|
|
46
|
+
actor: str,
|
|
47
|
+
field: str,
|
|
48
|
+
kid: str,
|
|
49
|
+
event_type: EventType,
|
|
50
|
+
outcome: Outcome,
|
|
51
|
+
**extra
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Log an audit event.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
actor: Identifier for the entity performing the action
|
|
58
|
+
field: Field name being accessed
|
|
59
|
+
kid: Key identifier used
|
|
60
|
+
event_type: Type of event
|
|
61
|
+
outcome: Outcome of the operation
|
|
62
|
+
**extra: Additional metadata to include in the log
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ConsoleAuditLogger(AuditLogger):
|
|
68
|
+
"""Audit logger that writes to console (stdout/stderr)."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, use_stderr: bool = False):
|
|
71
|
+
"""
|
|
72
|
+
Initialize the console audit logger.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
use_stderr: If True, write to stderr instead of stdout
|
|
76
|
+
"""
|
|
77
|
+
import sys
|
|
78
|
+
self._output = sys.stderr if use_stderr else sys.stdout
|
|
79
|
+
|
|
80
|
+
def log(
|
|
81
|
+
self,
|
|
82
|
+
actor: str,
|
|
83
|
+
field: str,
|
|
84
|
+
kid: str,
|
|
85
|
+
event_type: EventType,
|
|
86
|
+
outcome: Outcome,
|
|
87
|
+
**extra
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Log an audit event to console."""
|
|
90
|
+
event = {
|
|
91
|
+
"timestamp": time.time(),
|
|
92
|
+
"actor": actor,
|
|
93
|
+
"field": field,
|
|
94
|
+
"kid": kid,
|
|
95
|
+
"eventType": event_type.value if isinstance(event_type, EventType) else event_type,
|
|
96
|
+
"outcome": outcome.value if isinstance(outcome, Outcome) else outcome,
|
|
97
|
+
**extra
|
|
98
|
+
}
|
|
99
|
+
self._output.write(json.dumps(event) + "\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FileAuditLogger(AuditLogger):
|
|
103
|
+
"""Audit logger that writes to a file."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, file_path: str):
|
|
106
|
+
"""
|
|
107
|
+
Initialize the file audit logger.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
file_path: Path to the audit log file
|
|
111
|
+
"""
|
|
112
|
+
self._file_path = file_path
|
|
113
|
+
|
|
114
|
+
def log(
|
|
115
|
+
self,
|
|
116
|
+
actor: str,
|
|
117
|
+
field: str,
|
|
118
|
+
kid: str,
|
|
119
|
+
event_type: EventType,
|
|
120
|
+
outcome: Outcome,
|
|
121
|
+
**extra
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Log an audit event to file."""
|
|
124
|
+
event = {
|
|
125
|
+
"timestamp": time.time(),
|
|
126
|
+
"actor": actor,
|
|
127
|
+
"field": field,
|
|
128
|
+
"kid": kid,
|
|
129
|
+
"eventType": event_type.value if isinstance(event_type, EventType) else event_type,
|
|
130
|
+
"outcome": outcome.value if isinstance(outcome, Outcome) else outcome,
|
|
131
|
+
**extra
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
with open(self._file_path, "a", encoding="utf-8") as f:
|
|
136
|
+
f.write(json.dumps(event) + "\n")
|
|
137
|
+
except IOError as e:
|
|
138
|
+
# Fail silently to avoid breaking the application
|
|
139
|
+
# In production, you might want to log to stderr or a fallback
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class NullAuditLogger(AuditLogger):
|
|
144
|
+
"""
|
|
145
|
+
No-op audit logger that discards all events.
|
|
146
|
+
|
|
147
|
+
Use this for development/testing or when audit logging is not required.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def log(
|
|
151
|
+
self,
|
|
152
|
+
actor: str,
|
|
153
|
+
field: str,
|
|
154
|
+
kid: str,
|
|
155
|
+
event_type: EventType,
|
|
156
|
+
outcome: Outcome,
|
|
157
|
+
**extra
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Discard the log entry."""
|
|
160
|
+
pass
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Blind index module for PANDA SDK.
|
|
3
|
+
|
|
4
|
+
Provides deterministic hashing for searchable encrypted fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hmac
|
|
8
|
+
import hashlib
|
|
9
|
+
import base64
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlindIndexer:
|
|
14
|
+
"""
|
|
15
|
+
Deterministic indexing for encrypted fields using HMAC-SHA256.
|
|
16
|
+
|
|
17
|
+
Blind indexes enable exact-match searches on encrypted fields
|
|
18
|
+
without storing searchable plaintext or using deterministic encryption.
|
|
19
|
+
|
|
20
|
+
The index is computed as: HMAC-SHA256(blind_index_key, normalize(plaintext))
|
|
21
|
+
|
|
22
|
+
Security Note: Blind indexes leak equality patterns. An attacker who
|
|
23
|
+
gains database access can determine which records have the same value
|
|
24
|
+
for a field. Use with caution on low-cardinality fields.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Normalization modes
|
|
28
|
+
NORMALIZATION_DIGITS = "digits"
|
|
29
|
+
NORMALIZATION_LOWERCASE = "lowercase"
|
|
30
|
+
NORMALIZATION_EMAIL = "email"
|
|
31
|
+
NORMALIZATION_PHONE = "phone"
|
|
32
|
+
NORMALIZATION_NIK = "nik"
|
|
33
|
+
NORMALIZATION_TRIM = "trim"
|
|
34
|
+
NORMALIZATION_NONE = "none"
|
|
35
|
+
NORMALIZATION_RAW = "raw"
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def normalize(plaintext: str, normalization: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Normalize plaintext before generating blind index.
|
|
41
|
+
|
|
42
|
+
Normalization must be applied identically on both write and read paths
|
|
43
|
+
to ensure consistent index values.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
plaintext: String to normalize
|
|
47
|
+
normalization: Normalization mode
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Normalized string
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
>>> BlindIndexer.normalize(" Email@Example.COM ", "lowercase")
|
|
54
|
+
'email@example.com'
|
|
55
|
+
|
|
56
|
+
>>> BlindIndexer.normalize("+62-812-3456-7890", "digits")
|
|
57
|
+
'6281234567890'
|
|
58
|
+
"""
|
|
59
|
+
mode = normalization.lower()
|
|
60
|
+
|
|
61
|
+
if mode in (BlindIndexer.NORMALIZATION_DIGITS,
|
|
62
|
+
BlindIndexer.NORMALIZATION_PHONE,
|
|
63
|
+
BlindIndexer.NORMALIZATION_NIK):
|
|
64
|
+
# Extract only digits
|
|
65
|
+
import re
|
|
66
|
+
return re.sub(r"\D", "", plaintext)
|
|
67
|
+
|
|
68
|
+
elif mode in (BlindIndexer.NORMALIZATION_LOWERCASE,
|
|
69
|
+
BlindIndexer.NORMALIZATION_EMAIL):
|
|
70
|
+
# Lowercase and trim whitespace
|
|
71
|
+
return plaintext.strip().lower()
|
|
72
|
+
|
|
73
|
+
elif mode == BlindIndexer.NORMALIZATION_TRIM:
|
|
74
|
+
# Only trim whitespace
|
|
75
|
+
return plaintext.strip()
|
|
76
|
+
|
|
77
|
+
elif mode in (BlindIndexer.NORMALIZATION_NONE,
|
|
78
|
+
BlindIndexer.NORMALIZATION_RAW):
|
|
79
|
+
# No normalization
|
|
80
|
+
return plaintext
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
# Default to no normalization for unknown modes
|
|
84
|
+
return plaintext
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def generate(
|
|
88
|
+
plaintext: str,
|
|
89
|
+
key: Union[str, bytes],
|
|
90
|
+
normalization: str = "lowercase"
|
|
91
|
+
) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Generate a blind index for searchable encryption.
|
|
94
|
+
|
|
95
|
+
The blind index is a deterministic HMAC-SHA256 hash that can be
|
|
96
|
+
stored in a separate database column for exact-match queries.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
plaintext: Value to generate index for
|
|
100
|
+
key: Blind index key (distinct from encryption key!)
|
|
101
|
+
normalization: Normalization mode to apply
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Base64-encoded HMAC-SHA256 hash
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> BlindIndexer.generate("user@example.com", "my-blind-index-key", "lowercase")
|
|
108
|
+
'abc123def456...'
|
|
109
|
+
|
|
110
|
+
>>> # To search: WHERE blind_index_column = BlindIndexer.generate(query_value, key, "lowercase")
|
|
111
|
+
"""
|
|
112
|
+
# Normalize the plaintext
|
|
113
|
+
normalized = BlindIndexer.normalize(plaintext, normalization)
|
|
114
|
+
|
|
115
|
+
# Convert key to bytes if needed
|
|
116
|
+
if isinstance(key, str):
|
|
117
|
+
key_bytes = key.encode("utf-8")
|
|
118
|
+
else:
|
|
119
|
+
key_bytes = key
|
|
120
|
+
|
|
121
|
+
# Compute HMAC-SHA256
|
|
122
|
+
hmac_obj = hmac.new(key_bytes, normalized.encode("utf-8"), hashlib.sha256)
|
|
123
|
+
digest = hmac_obj.digest()
|
|
124
|
+
|
|
125
|
+
# Return base64-encoded digest
|
|
126
|
+
return base64.b64encode(digest).decode("ascii")
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def validate_key(key: Union[str, bytes]) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Validate that a blind index key meets security requirements.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
key: Key to validate
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if key is valid, False otherwise
|
|
138
|
+
"""
|
|
139
|
+
if isinstance(key, str):
|
|
140
|
+
key_bytes = key.encode("utf-8")
|
|
141
|
+
else:
|
|
142
|
+
key_bytes = key
|
|
143
|
+
|
|
144
|
+
# Key should be at least 16 bytes (128 bits)
|
|
145
|
+
return len(key_bytes) >= 16
|