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 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
+ ]
@@ -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,11 @@
1
+ """
2
+ Blind index module for PANDA SDK.
3
+
4
+ Provides deterministic indexing for searchable encrypted fields.
5
+ """
6
+
7
+ from .blind_index import BlindIndexer
8
+
9
+ __all__ = [
10
+ "BlindIndexer",
11
+ ]
@@ -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
panda/cli/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ CLI module for PANDA SDK.
3
+
4
+ Provides command-line interface for initialization and key management.
5
+ """
6
+
7
+ __all__ = []