otdf-python 0.1.10__py3-none-any.whl → 0.3.5__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.
- otdf_python/__init__.py +25 -0
- otdf_python/__main__.py +12 -0
- otdf_python/address_normalizer.py +84 -0
- otdf_python/aesgcm.py +55 -0
- otdf_python/assertion_config.py +84 -0
- otdf_python/asym_crypto.py +198 -0
- otdf_python/auth_headers.py +33 -0
- otdf_python/autoconfigure_utils.py +113 -0
- otdf_python/cli.py +569 -0
- otdf_python/collection_store.py +41 -0
- otdf_python/collection_store_impl.py +22 -0
- otdf_python/config.py +69 -0
- otdf_python/connect_client.py +0 -0
- otdf_python/constants.py +1 -0
- otdf_python/crypto_utils.py +78 -0
- otdf_python/dpop.py +81 -0
- otdf_python/ecc_constants.py +176 -0
- otdf_python/ecc_mode.py +83 -0
- otdf_python/ecdh.py +317 -0
- otdf_python/eckeypair.py +75 -0
- otdf_python/header.py +181 -0
- otdf_python/invalid_zip_exception.py +8 -0
- otdf_python/kas_client.py +709 -0
- otdf_python/kas_connect_rpc_client.py +213 -0
- otdf_python/kas_info.py +25 -0
- otdf_python/kas_key_cache.py +52 -0
- otdf_python/key_type.py +31 -0
- otdf_python/key_type_constants.py +43 -0
- otdf_python/manifest.py +215 -0
- otdf_python/nanotdf.py +863 -0
- otdf_python/nanotdf_ecdsa_struct.py +132 -0
- otdf_python/nanotdf_type.py +43 -0
- otdf_python/policy_binding_serializer.py +39 -0
- otdf_python/policy_info.py +55 -0
- otdf_python/policy_object.py +22 -0
- otdf_python/policy_stub.py +2 -0
- otdf_python/resource_locator.py +172 -0
- otdf_python/sdk.py +436 -0
- otdf_python/sdk_builder.py +416 -0
- otdf_python/sdk_exceptions.py +16 -0
- otdf_python/symmetric_and_payload_config.py +30 -0
- otdf_python/tdf.py +480 -0
- otdf_python/tdf_reader.py +153 -0
- otdf_python/tdf_writer.py +23 -0
- otdf_python/token_source.py +34 -0
- otdf_python/version.py +57 -0
- otdf_python/zip_reader.py +47 -0
- otdf_python/zip_writer.py +70 -0
- otdf_python-0.3.5.dist-info/METADATA +153 -0
- otdf_python-0.3.5.dist-info/RECORD +137 -0
- {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info}/WHEEL +1 -2
- {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info/licenses}/LICENSE +1 -1
- otdf_python_proto/__init__.py +37 -0
- otdf_python_proto/authorization/__init__.py +1 -0
- otdf_python_proto/authorization/authorization_pb2.py +80 -0
- otdf_python_proto/authorization/authorization_pb2.pyi +161 -0
- otdf_python_proto/authorization/authorization_pb2_connect.py +191 -0
- otdf_python_proto/authorization/v2/authorization_pb2.py +105 -0
- otdf_python_proto/authorization/v2/authorization_pb2.pyi +134 -0
- otdf_python_proto/authorization/v2/authorization_pb2_connect.py +233 -0
- otdf_python_proto/common/__init__.py +1 -0
- otdf_python_proto/common/common_pb2.py +52 -0
- otdf_python_proto/common/common_pb2.pyi +61 -0
- otdf_python_proto/entity/__init__.py +1 -0
- otdf_python_proto/entity/entity_pb2.py +47 -0
- otdf_python_proto/entity/entity_pb2.pyi +50 -0
- otdf_python_proto/entityresolution/__init__.py +1 -0
- otdf_python_proto/entityresolution/entity_resolution_pb2.py +57 -0
- otdf_python_proto/entityresolution/entity_resolution_pb2.pyi +55 -0
- otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +149 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2.py +55 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2.pyi +55 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +149 -0
- otdf_python_proto/kas/__init__.py +9 -0
- otdf_python_proto/kas/kas_pb2.py +103 -0
- otdf_python_proto/kas/kas_pb2.pyi +170 -0
- otdf_python_proto/kas/kas_pb2_connect.py +192 -0
- otdf_python_proto/legacy_grpc/__init__.py +1 -0
- otdf_python_proto/legacy_grpc/authorization/authorization_pb2_grpc.py +163 -0
- otdf_python_proto/legacy_grpc/authorization/v2/authorization_pb2_grpc.py +206 -0
- otdf_python_proto/legacy_grpc/common/common_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/entity/entity_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/entityresolution/entity_resolution_pb2_grpc.py +122 -0
- otdf_python_proto/legacy_grpc/entityresolution/v2/entity_resolution_pb2_grpc.py +120 -0
- otdf_python_proto/legacy_grpc/kas/kas_pb2_grpc.py +172 -0
- otdf_python_proto/legacy_grpc/logger/audit/test_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/policy/actions/actions_pb2_grpc.py +249 -0
- otdf_python_proto/legacy_grpc/policy/attributes/attributes_pb2_grpc.py +873 -0
- otdf_python_proto/legacy_grpc/policy/kasregistry/key_access_server_registry_pb2_grpc.py +602 -0
- otdf_python_proto/legacy_grpc/policy/keymanagement/key_management_pb2_grpc.py +251 -0
- otdf_python_proto/legacy_grpc/policy/namespaces/namespaces_pb2_grpc.py +427 -0
- otdf_python_proto/legacy_grpc/policy/objects_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/policy/registeredresources/registered_resources_pb2_grpc.py +524 -0
- otdf_python_proto/legacy_grpc/policy/resourcemapping/resource_mapping_pb2_grpc.py +516 -0
- otdf_python_proto/legacy_grpc/policy/selectors_pb2_grpc.py +4 -0
- otdf_python_proto/legacy_grpc/policy/subjectmapping/subject_mapping_pb2_grpc.py +551 -0
- otdf_python_proto/legacy_grpc/policy/unsafe/unsafe_pb2_grpc.py +485 -0
- otdf_python_proto/legacy_grpc/wellknownconfiguration/wellknown_configuration_pb2_grpc.py +77 -0
- otdf_python_proto/logger/__init__.py +1 -0
- otdf_python_proto/logger/audit/test_pb2.py +43 -0
- otdf_python_proto/logger/audit/test_pb2.pyi +45 -0
- otdf_python_proto/policy/__init__.py +1 -0
- otdf_python_proto/policy/actions/actions_pb2.py +75 -0
- otdf_python_proto/policy/actions/actions_pb2.pyi +87 -0
- otdf_python_proto/policy/actions/actions_pb2_connect.py +275 -0
- otdf_python_proto/policy/attributes/attributes_pb2.py +234 -0
- otdf_python_proto/policy/attributes/attributes_pb2.pyi +328 -0
- otdf_python_proto/policy/attributes/attributes_pb2_connect.py +863 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.py +266 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.pyi +450 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +611 -0
- otdf_python_proto/policy/keymanagement/key_management_pb2.py +79 -0
- otdf_python_proto/policy/keymanagement/key_management_pb2.pyi +87 -0
- otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +275 -0
- otdf_python_proto/policy/namespaces/namespaces_pb2.py +117 -0
- otdf_python_proto/policy/namespaces/namespaces_pb2.pyi +147 -0
- otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +443 -0
- otdf_python_proto/policy/objects_pb2.py +150 -0
- otdf_python_proto/policy/objects_pb2.pyi +464 -0
- otdf_python_proto/policy/registeredresources/registered_resources_pb2.py +139 -0
- otdf_python_proto/policy/registeredresources/registered_resources_pb2.pyi +196 -0
- otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +527 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.py +139 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.pyi +194 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +527 -0
- otdf_python_proto/policy/selectors_pb2.py +57 -0
- otdf_python_proto/policy/selectors_pb2.pyi +90 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.py +127 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.pyi +189 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +569 -0
- otdf_python_proto/policy/unsafe/unsafe_pb2.py +113 -0
- otdf_python_proto/policy/unsafe/unsafe_pb2.pyi +145 -0
- otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +485 -0
- otdf_python_proto/wellknownconfiguration/__init__.py +1 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py +51 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi +32 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +107 -0
- otdf_python/_gotdf_python.cpython-312-darwin.so +0 -0
- otdf_python/build.py +0 -190
- otdf_python/go.py +0 -1478
- otdf_python/gotdf_python.py +0 -383
- otdf_python-0.1.10.dist-info/METADATA +0 -149
- otdf_python-0.1.10.dist-info/RECORD +0 -10
- otdf_python-0.1.10.dist-info/top_level.txt +0 -1
otdf_python/__init__.py
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTDF Python SDK
|
|
3
|
+
|
|
4
|
+
A Python implementation of the OpenTDF SDK for working with Trusted Data Format (TDF) files.
|
|
5
|
+
Provides both programmatic APIs and command-line interface for encryption and decryption.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cli import main as cli_main
|
|
9
|
+
from .config import KASInfo, NanoTDFConfig, TDFConfig
|
|
10
|
+
from .sdk import SDK
|
|
11
|
+
from .sdk_builder import SDKBuilder
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SDK",
|
|
15
|
+
"KASInfo",
|
|
16
|
+
"NanoTDFConfig",
|
|
17
|
+
"SDKBuilder",
|
|
18
|
+
"TDFConfig",
|
|
19
|
+
"cli_main",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
"""Entry point for the CLI."""
|
|
25
|
+
cli_main()
|
otdf_python/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Main entry point for running otdf_python as a module.
|
|
4
|
+
|
|
5
|
+
This allows the package to be run with `python -m otdf_python` and properly
|
|
6
|
+
handles the CLI interface without import conflicts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .cli import main
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Address normalization utilities for OpenTDF.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
from .sdk_exceptions import SDKException
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_address(url_string: str, use_plaintext: bool) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Normalize a URL address to ensure it has the correct scheme and port.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
url_string: The URL string to normalize
|
|
20
|
+
use_plaintext: If True, use http scheme, otherwise use https
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The normalized URL string
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
SDKException: If there's an error parsing or creating the URL
|
|
27
|
+
"""
|
|
28
|
+
scheme = "http" if use_plaintext else "https"
|
|
29
|
+
|
|
30
|
+
# Check if we have a host:port format without scheme (with non-digit port)
|
|
31
|
+
host_port_pattern = re.match(r"^([^/:]+):([^/]+)$", url_string)
|
|
32
|
+
if host_port_pattern:
|
|
33
|
+
host = host_port_pattern.group(1)
|
|
34
|
+
port_str = host_port_pattern.group(2)
|
|
35
|
+
try:
|
|
36
|
+
port = int(port_str)
|
|
37
|
+
except ValueError:
|
|
38
|
+
raise SDKException(f"Invalid port in URL [{url_string}]")
|
|
39
|
+
|
|
40
|
+
normalized_url = f"{scheme}://{host}:{port}"
|
|
41
|
+
logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
|
|
42
|
+
return normalized_url
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Check if we just have a hostname without scheme and port
|
|
46
|
+
if "://" not in url_string and "/" not in url_string and ":" not in url_string:
|
|
47
|
+
port = 80 if use_plaintext else 443
|
|
48
|
+
normalized_url = f"{scheme}://{url_string}:{port}"
|
|
49
|
+
logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
|
|
50
|
+
return normalized_url
|
|
51
|
+
|
|
52
|
+
# Parse the URL
|
|
53
|
+
parsed_url = urlparse(url_string)
|
|
54
|
+
|
|
55
|
+
# If no scheme, add one
|
|
56
|
+
if not parsed_url.scheme:
|
|
57
|
+
url_string = f"{scheme}://{url_string}"
|
|
58
|
+
parsed_url = urlparse(url_string)
|
|
59
|
+
|
|
60
|
+
# Extract host and port
|
|
61
|
+
host = parsed_url.netloc.split(":")[0] if parsed_url.netloc else parsed_url.path
|
|
62
|
+
|
|
63
|
+
# If there's a port in the URL, try to extract it
|
|
64
|
+
port = None
|
|
65
|
+
if ":" in parsed_url.netloc:
|
|
66
|
+
_, port_str = parsed_url.netloc.split(":", 1)
|
|
67
|
+
try:
|
|
68
|
+
port = int(port_str)
|
|
69
|
+
except ValueError:
|
|
70
|
+
raise SDKException(f"Invalid port in URL [{url_string}]")
|
|
71
|
+
|
|
72
|
+
# If no port was found or extracted, use the default
|
|
73
|
+
if port is None:
|
|
74
|
+
port = 80 if use_plaintext else 443
|
|
75
|
+
|
|
76
|
+
# Reconstruct the URL with the desired scheme
|
|
77
|
+
normalized_url = f"{scheme}://{host}:{port}"
|
|
78
|
+
logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
|
|
79
|
+
return normalized_url
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
if isinstance(e, SDKException):
|
|
83
|
+
raise e
|
|
84
|
+
raise SDKException(f"Error normalizing URL [{url_string}]", e)
|
otdf_python/aesgcm.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AesGcm:
|
|
7
|
+
GCM_NONCE_LENGTH = 12
|
|
8
|
+
GCM_TAG_LENGTH = 16
|
|
9
|
+
|
|
10
|
+
def __init__(self, key: bytes):
|
|
11
|
+
if not key or len(key) not in (16, 24, 32):
|
|
12
|
+
raise ValueError("Invalid key size for GCM encryption")
|
|
13
|
+
self.key = key
|
|
14
|
+
self.aesgcm = AESGCM(key)
|
|
15
|
+
|
|
16
|
+
def get_key(self) -> bytes:
|
|
17
|
+
return self.key
|
|
18
|
+
|
|
19
|
+
class Encrypted:
|
|
20
|
+
def __init__(self, iv: bytes, ciphertext: bytes):
|
|
21
|
+
self.iv = iv
|
|
22
|
+
self.ciphertext = ciphertext
|
|
23
|
+
|
|
24
|
+
def as_bytes(self) -> bytes:
|
|
25
|
+
return self.iv + self.ciphertext
|
|
26
|
+
|
|
27
|
+
def encrypt(
|
|
28
|
+
self, plaintext: bytes, offset: int = 0, length: int | None = None
|
|
29
|
+
) -> "AesGcm.Encrypted":
|
|
30
|
+
if length is None:
|
|
31
|
+
length = len(plaintext) - offset
|
|
32
|
+
iv = os.urandom(self.GCM_NONCE_LENGTH)
|
|
33
|
+
ct = self.aesgcm.encrypt(iv, plaintext[offset : offset + length], None)
|
|
34
|
+
return self.Encrypted(iv, ct)
|
|
35
|
+
|
|
36
|
+
def encrypt_with_iv(
|
|
37
|
+
self,
|
|
38
|
+
iv: bytes,
|
|
39
|
+
auth_tag_len: int,
|
|
40
|
+
plaintext: bytes,
|
|
41
|
+
offset: int = 0,
|
|
42
|
+
length: int | None = None,
|
|
43
|
+
) -> bytes:
|
|
44
|
+
if length is None:
|
|
45
|
+
length = len(plaintext) - offset
|
|
46
|
+
ct = self.aesgcm.encrypt(iv, plaintext[offset : offset + length], None)
|
|
47
|
+
return iv + ct
|
|
48
|
+
|
|
49
|
+
def decrypt(self, encrypted: "AesGcm.Encrypted") -> bytes:
|
|
50
|
+
return self.aesgcm.decrypt(encrypted.iv, encrypted.ciphertext, None)
|
|
51
|
+
|
|
52
|
+
def decrypt_with_iv(
|
|
53
|
+
self, iv: bytes, auth_tag_len: int, cipher_data: bytes
|
|
54
|
+
) -> bytes:
|
|
55
|
+
return self.aesgcm.decrypt(iv, cipher_data, None)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Type(Enum):
|
|
6
|
+
HANDLING_ASSERTION = "handling"
|
|
7
|
+
BASE_ASSERTION = "base"
|
|
8
|
+
|
|
9
|
+
def __str__(self):
|
|
10
|
+
return self.value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Scope(Enum):
|
|
14
|
+
TRUSTED_DATA_OBJ = "tdo"
|
|
15
|
+
PAYLOAD = "payload"
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return self.value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AssertionKeyAlg(Enum):
|
|
22
|
+
RS256 = auto()
|
|
23
|
+
HS256 = auto()
|
|
24
|
+
NOT_DEFINED = auto()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AppliesToState(Enum):
|
|
28
|
+
ENCRYPTED = "encrypted"
|
|
29
|
+
UNENCRYPTED = "unencrypted"
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
return self.value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BindingMethod(Enum):
|
|
36
|
+
JWS = "jws"
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return self.value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AssertionKey:
|
|
43
|
+
def __init__(self, alg: AssertionKeyAlg, key: Any):
|
|
44
|
+
self.alg = alg
|
|
45
|
+
self.key = key
|
|
46
|
+
|
|
47
|
+
def is_defined(self):
|
|
48
|
+
return self.alg != AssertionKeyAlg.NOT_DEFINED
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Statement:
|
|
52
|
+
def __init__(self, format: str, schema: str, value: str):
|
|
53
|
+
self.format = format
|
|
54
|
+
self.schema = schema
|
|
55
|
+
self.value = value
|
|
56
|
+
|
|
57
|
+
def __eq__(self, other):
|
|
58
|
+
return (
|
|
59
|
+
isinstance(other, Statement)
|
|
60
|
+
and self.format == other.format
|
|
61
|
+
and self.schema == other.schema
|
|
62
|
+
and self.value == other.value
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __hash__(self):
|
|
66
|
+
return hash((self.format, self.schema, self.value))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AssertionConfig:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
id: str,
|
|
73
|
+
type: Type,
|
|
74
|
+
scope: Scope,
|
|
75
|
+
applies_to_state: AppliesToState,
|
|
76
|
+
statement: Statement,
|
|
77
|
+
signing_key: AssertionKey | None = None,
|
|
78
|
+
):
|
|
79
|
+
self.id = id
|
|
80
|
+
self.type = type
|
|
81
|
+
self.scope = scope
|
|
82
|
+
self.applies_to_state = applies_to_state
|
|
83
|
+
self.statement = statement
|
|
84
|
+
self.signing_key = signing_key
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asymmetric encryption and decryption utilities for RSA keys in PEM format.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
11
|
+
from cryptography.x509 import load_pem_x509_certificate
|
|
12
|
+
|
|
13
|
+
from .sdk_exceptions import SDKException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsymDecryption:
|
|
17
|
+
"""
|
|
18
|
+
Provides functionality for asymmetric decryption using an RSA private key.
|
|
19
|
+
|
|
20
|
+
Supports both PEM string and key object initialization for flexibility.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
|
|
24
|
+
PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"
|
|
25
|
+
PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----"
|
|
26
|
+
|
|
27
|
+
def __init__(self, private_key_pem: str | None = None, private_key_obj=None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize with either a PEM string or a key object.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
private_key_pem: Private key in PEM format (with or without headers)
|
|
33
|
+
private_key_obj: Pre-loaded private key object from cryptography library
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
SDKException: If key loading fails
|
|
37
|
+
"""
|
|
38
|
+
if private_key_obj is not None:
|
|
39
|
+
self.private_key = private_key_obj
|
|
40
|
+
elif private_key_pem is not None:
|
|
41
|
+
try:
|
|
42
|
+
# Try direct PEM loading first (most common case)
|
|
43
|
+
try:
|
|
44
|
+
self.private_key = serialization.load_pem_private_key(
|
|
45
|
+
private_key_pem.encode(),
|
|
46
|
+
password=None,
|
|
47
|
+
backend=default_backend(),
|
|
48
|
+
)
|
|
49
|
+
except Exception:
|
|
50
|
+
# Fallback: strip headers and load as DER (for base64-only keys)
|
|
51
|
+
private_key_pem = (
|
|
52
|
+
private_key_pem.replace(self.PRIVATE_KEY_HEADER, "")
|
|
53
|
+
.replace(self.PRIVATE_KEY_FOOTER, "")
|
|
54
|
+
.replace("\n", "")
|
|
55
|
+
.replace("\r", "")
|
|
56
|
+
.replace(" ", "")
|
|
57
|
+
)
|
|
58
|
+
decoded = base64.b64decode(private_key_pem)
|
|
59
|
+
self.private_key = serialization.load_der_private_key(
|
|
60
|
+
decoded, password=None, backend=default_backend()
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise SDKException(f"Failed to load private key: {e}")
|
|
64
|
+
else:
|
|
65
|
+
self.private_key = None
|
|
66
|
+
|
|
67
|
+
def decrypt(self, data: bytes) -> bytes:
|
|
68
|
+
"""
|
|
69
|
+
Decrypt data using RSA OAEP with SHA-1.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
data: Encrypted bytes to decrypt
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Decrypted bytes
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
SDKException: If decryption fails or key is not set
|
|
79
|
+
"""
|
|
80
|
+
if self.private_key is None:
|
|
81
|
+
raise SDKException("Failed to decrypt, private key is empty")
|
|
82
|
+
try:
|
|
83
|
+
return self.private_key.decrypt(
|
|
84
|
+
data,
|
|
85
|
+
padding.OAEP(
|
|
86
|
+
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
|
87
|
+
algorithm=hashes.SHA1(),
|
|
88
|
+
label=None,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise SDKException(f"Error performing decryption: {e}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AsymEncryption:
|
|
96
|
+
"""
|
|
97
|
+
Provides functionality for asymmetric encryption using an RSA public key or certificate in PEM format.
|
|
98
|
+
|
|
99
|
+
Supports PEM public keys, X.509 certificates, and pre-loaded key objects.
|
|
100
|
+
Also handles base64-encoded keys without PEM headers.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----"
|
|
104
|
+
PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----"
|
|
105
|
+
CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
|
|
106
|
+
|
|
107
|
+
def __init__(self, public_key_pem: str | None = None, public_key_obj=None):
|
|
108
|
+
"""
|
|
109
|
+
Initialize with either a PEM string or a key object.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
public_key_pem: Public key in PEM format, X.509 certificate, or base64 string
|
|
113
|
+
public_key_obj: Pre-loaded public key object from cryptography library
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
SDKException: If key loading fails or key is not RSA
|
|
117
|
+
"""
|
|
118
|
+
if public_key_obj is not None:
|
|
119
|
+
self.public_key = public_key_obj
|
|
120
|
+
elif public_key_pem is not None:
|
|
121
|
+
try:
|
|
122
|
+
if "BEGIN CERTIFICATE" in public_key_pem:
|
|
123
|
+
# Load from X.509 certificate
|
|
124
|
+
cert = load_pem_x509_certificate(
|
|
125
|
+
public_key_pem.encode(), default_backend()
|
|
126
|
+
)
|
|
127
|
+
self.public_key = cert.public_key()
|
|
128
|
+
else:
|
|
129
|
+
# Try direct PEM loading first (most common case)
|
|
130
|
+
try:
|
|
131
|
+
self.public_key = serialization.load_pem_public_key(
|
|
132
|
+
public_key_pem.encode(), backend=default_backend()
|
|
133
|
+
)
|
|
134
|
+
except Exception:
|
|
135
|
+
# Fallback: strip headers and load as DER (for base64-only keys)
|
|
136
|
+
pem_body = re.sub(r"-----BEGIN (.*)-----", "", public_key_pem)
|
|
137
|
+
pem_body = re.sub(r"-----END (.*)-----", "", pem_body)
|
|
138
|
+
pem_body = re.sub(r"\s", "", pem_body)
|
|
139
|
+
decoded = base64.b64decode(pem_body)
|
|
140
|
+
self.public_key = serialization.load_der_public_key(
|
|
141
|
+
decoded, backend=default_backend()
|
|
142
|
+
)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise SDKException(f"Failed to load public key: {e}")
|
|
145
|
+
else:
|
|
146
|
+
self.public_key = None
|
|
147
|
+
|
|
148
|
+
# Validate that it's an RSA key
|
|
149
|
+
if self.public_key is not None and not isinstance(
|
|
150
|
+
self.public_key, rsa.RSAPublicKey
|
|
151
|
+
):
|
|
152
|
+
raise SDKException("Not an RSA PEM formatted public key")
|
|
153
|
+
|
|
154
|
+
def encrypt(self, data: bytes) -> bytes:
|
|
155
|
+
"""
|
|
156
|
+
Encrypt data using RSA OAEP with SHA-1.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
data: Plaintext bytes to encrypt
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Encrypted bytes
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
SDKException: If encryption fails or key is not set
|
|
166
|
+
"""
|
|
167
|
+
if self.public_key is None:
|
|
168
|
+
raise SDKException("Failed to encrypt, public key is empty")
|
|
169
|
+
try:
|
|
170
|
+
return self.public_key.encrypt(
|
|
171
|
+
data,
|
|
172
|
+
padding.OAEP(
|
|
173
|
+
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
|
174
|
+
algorithm=hashes.SHA1(),
|
|
175
|
+
label=None,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
raise SDKException(f"Error performing encryption: {e}")
|
|
180
|
+
|
|
181
|
+
def public_key_in_pem_format(self) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Export the public key to PEM format.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Public key as PEM-encoded string
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
SDKException: If export fails
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
pem = self.public_key.public_bytes(
|
|
193
|
+
encoding=serialization.Encoding.PEM,
|
|
194
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
195
|
+
)
|
|
196
|
+
return pem.decode()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
raise SDKException(f"Error exporting public key to PEM: {e}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class AuthHeaders:
|
|
6
|
+
"""
|
|
7
|
+
Represents authentication headers used in token-based authorization.
|
|
8
|
+
This class holds authorization and DPoP (Demonstrating Proof of Possession) headers
|
|
9
|
+
that are used in token-based API requests.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
auth_header: str
|
|
13
|
+
dpop_header: str = ""
|
|
14
|
+
|
|
15
|
+
def get_auth_header(self) -> str:
|
|
16
|
+
"""Returns the authorization header."""
|
|
17
|
+
return self.auth_header
|
|
18
|
+
|
|
19
|
+
def get_dpop_header(self) -> str:
|
|
20
|
+
"""Returns the DPoP header."""
|
|
21
|
+
return self.dpop_header
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, str]:
|
|
24
|
+
"""
|
|
25
|
+
Convert authentication headers to a dictionary for use with HTTP clients.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary with 'Authorization' header and optionally 'DPoP' header
|
|
29
|
+
"""
|
|
30
|
+
headers = {"Authorization": self.auth_header}
|
|
31
|
+
if self.dpop_header:
|
|
32
|
+
headers["DPoP"] = self.dpop_header
|
|
33
|
+
return headers
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import urllib.parse
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# RuleType constants
|
|
8
|
+
class RuleType:
|
|
9
|
+
HIERARCHY = "hierarchy"
|
|
10
|
+
ALL_OF = "allOf"
|
|
11
|
+
ANY_OF = "anyOf"
|
|
12
|
+
UNSPECIFIED = "unspecified"
|
|
13
|
+
EMPTY_TERM = "DEFAULT"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class KeySplitStep:
|
|
18
|
+
kas: str
|
|
19
|
+
splitID: str
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"KeySplitStep{{kas={self.kas}, splitID={self.splitID}}}"
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other: Any) -> bool:
|
|
25
|
+
if not isinstance(other, KeySplitStep):
|
|
26
|
+
return False
|
|
27
|
+
return self.kas == other.kas and self.splitID == other.splitID
|
|
28
|
+
|
|
29
|
+
def __hash__(self):
|
|
30
|
+
return hash((self.kas, self.splitID))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AutoConfigureException(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AttributeNameFQN:
|
|
38
|
+
def __init__(self, url: str):
|
|
39
|
+
pattern = re.compile(r"^(https?://[\w./-]+)/attr/([^/\s]*)$")
|
|
40
|
+
matcher = pattern.match(url)
|
|
41
|
+
if not matcher or not matcher.group(1) or not matcher.group(2):
|
|
42
|
+
raise AutoConfigureException("invalid type: attribute regex fail")
|
|
43
|
+
try:
|
|
44
|
+
urllib.parse.unquote(matcher.group(2))
|
|
45
|
+
except Exception:
|
|
46
|
+
raise AutoConfigureException(
|
|
47
|
+
f"invalid type: error in attribute name [{matcher.group(2)}]"
|
|
48
|
+
)
|
|
49
|
+
self.url = url
|
|
50
|
+
self.key = url.lower()
|
|
51
|
+
|
|
52
|
+
def __str__(self):
|
|
53
|
+
return self.url
|
|
54
|
+
|
|
55
|
+
def select(self, value: str):
|
|
56
|
+
new_url = f"{self.url}/value/{urllib.parse.quote(value)}"
|
|
57
|
+
return AttributeValueFQN(new_url)
|
|
58
|
+
|
|
59
|
+
def prefix(self):
|
|
60
|
+
return self.url
|
|
61
|
+
|
|
62
|
+
def get_key(self):
|
|
63
|
+
return self.key
|
|
64
|
+
|
|
65
|
+
def authority(self):
|
|
66
|
+
pattern = re.compile(r"^(https?://[\w./-]+)/attr/[^/\s]*$")
|
|
67
|
+
matcher = pattern.match(self.url)
|
|
68
|
+
if not matcher:
|
|
69
|
+
raise AutoConfigureException("invalid type")
|
|
70
|
+
return matcher.group(1)
|
|
71
|
+
|
|
72
|
+
def name(self):
|
|
73
|
+
pattern = re.compile(r"^https?://[\w./-]+/attr/([^/\s]*)$")
|
|
74
|
+
matcher = pattern.match(self.url)
|
|
75
|
+
if not matcher:
|
|
76
|
+
raise AutoConfigureException("invalid attribute")
|
|
77
|
+
try:
|
|
78
|
+
return urllib.parse.unquote(matcher.group(1))
|
|
79
|
+
except Exception:
|
|
80
|
+
raise AutoConfigureException("invalid type")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AttributeValueFQN:
|
|
84
|
+
def __init__(self, url: str):
|
|
85
|
+
pattern = re.compile(r"^(https?://[\w./-]+)/attr/(\S*)/value/(\S*)$")
|
|
86
|
+
matcher = pattern.match(url)
|
|
87
|
+
if (
|
|
88
|
+
not matcher
|
|
89
|
+
or not matcher.group(1)
|
|
90
|
+
or not matcher.group(2)
|
|
91
|
+
or not matcher.group(3)
|
|
92
|
+
):
|
|
93
|
+
raise AutoConfigureException(
|
|
94
|
+
f"invalid type: attribute regex fail for [{url}]"
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
urllib.parse.unquote(matcher.group(2))
|
|
98
|
+
urllib.parse.unquote(matcher.group(3))
|
|
99
|
+
except Exception:
|
|
100
|
+
raise AutoConfigureException("invalid type: error in attribute or value")
|
|
101
|
+
self.url = url
|
|
102
|
+
self.key = url.lower()
|
|
103
|
+
|
|
104
|
+
def __str__(self):
|
|
105
|
+
return self.url
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other: Any) -> bool:
|
|
108
|
+
if not isinstance(other, AttributeValueFQN):
|
|
109
|
+
return False
|
|
110
|
+
return self.key == other.key
|
|
111
|
+
|
|
112
|
+
def __hash__(self):
|
|
113
|
+
return hash(self.key)
|