otdf-python 0.1.10__py3-none-any.whl → 0.3.1__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.
Files changed (144) hide show
  1. otdf_python/__init__.py +25 -0
  2. otdf_python/__main__.py +12 -0
  3. otdf_python/address_normalizer.py +84 -0
  4. otdf_python/aesgcm.py +55 -0
  5. otdf_python/assertion_config.py +84 -0
  6. otdf_python/asym_crypto.py +85 -0
  7. otdf_python/asym_decryption.py +53 -0
  8. otdf_python/asym_encryption.py +75 -0
  9. otdf_python/auth_headers.py +21 -0
  10. otdf_python/autoconfigure_utils.py +113 -0
  11. otdf_python/cli.py +570 -0
  12. otdf_python/collection_store.py +41 -0
  13. otdf_python/collection_store_impl.py +22 -0
  14. otdf_python/config.py +69 -0
  15. otdf_python/connect_client.py +0 -0
  16. otdf_python/constants.py +1 -0
  17. otdf_python/crypto_utils.py +78 -0
  18. otdf_python/dpop.py +81 -0
  19. otdf_python/ecc_mode.py +32 -0
  20. otdf_python/eckeypair.py +75 -0
  21. otdf_python/header.py +143 -0
  22. otdf_python/invalid_zip_exception.py +8 -0
  23. otdf_python/kas_client.py +603 -0
  24. otdf_python/kas_connect_rpc_client.py +207 -0
  25. otdf_python/kas_info.py +25 -0
  26. otdf_python/kas_key_cache.py +52 -0
  27. otdf_python/key_type.py +31 -0
  28. otdf_python/key_type_constants.py +43 -0
  29. otdf_python/manifest.py +215 -0
  30. otdf_python/nanotdf.py +553 -0
  31. otdf_python/nanotdf_ecdsa_struct.py +132 -0
  32. otdf_python/nanotdf_type.py +43 -0
  33. otdf_python/policy_binding_serializer.py +39 -0
  34. otdf_python/policy_info.py +78 -0
  35. otdf_python/policy_object.py +22 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +44 -0
  38. otdf_python/sdk.py +528 -0
  39. otdf_python/sdk_builder.py +448 -0
  40. otdf_python/sdk_exceptions.py +16 -0
  41. otdf_python/symmetric_and_payload_config.py +30 -0
  42. otdf_python/tdf.py +479 -0
  43. otdf_python/tdf_reader.py +153 -0
  44. otdf_python/tdf_writer.py +23 -0
  45. otdf_python/token_source.py +34 -0
  46. otdf_python/version.py +57 -0
  47. otdf_python/zip_reader.py +47 -0
  48. otdf_python/zip_writer.py +70 -0
  49. otdf_python-0.3.1.dist-info/METADATA +231 -0
  50. otdf_python-0.3.1.dist-info/RECORD +137 -0
  51. {otdf_python-0.1.10.dist-info → otdf_python-0.3.1.dist-info}/WHEEL +1 -2
  52. {otdf_python-0.1.10.dist-info → otdf_python-0.3.1.dist-info/licenses}/LICENSE +1 -1
  53. otdf_python_proto/__init__.py +37 -0
  54. otdf_python_proto/authorization/__init__.py +1 -0
  55. otdf_python_proto/authorization/authorization_pb2.py +80 -0
  56. otdf_python_proto/authorization/authorization_pb2.pyi +161 -0
  57. otdf_python_proto/authorization/authorization_pb2_connect.py +191 -0
  58. otdf_python_proto/authorization/v2/authorization_pb2.py +105 -0
  59. otdf_python_proto/authorization/v2/authorization_pb2.pyi +134 -0
  60. otdf_python_proto/authorization/v2/authorization_pb2_connect.py +233 -0
  61. otdf_python_proto/common/__init__.py +1 -0
  62. otdf_python_proto/common/common_pb2.py +52 -0
  63. otdf_python_proto/common/common_pb2.pyi +61 -0
  64. otdf_python_proto/entity/__init__.py +1 -0
  65. otdf_python_proto/entity/entity_pb2.py +47 -0
  66. otdf_python_proto/entity/entity_pb2.pyi +50 -0
  67. otdf_python_proto/entityresolution/__init__.py +1 -0
  68. otdf_python_proto/entityresolution/entity_resolution_pb2.py +57 -0
  69. otdf_python_proto/entityresolution/entity_resolution_pb2.pyi +55 -0
  70. otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +149 -0
  71. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.py +55 -0
  72. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.pyi +55 -0
  73. otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +149 -0
  74. otdf_python_proto/kas/__init__.py +9 -0
  75. otdf_python_proto/kas/kas_pb2.py +103 -0
  76. otdf_python_proto/kas/kas_pb2.pyi +170 -0
  77. otdf_python_proto/kas/kas_pb2_connect.py +192 -0
  78. otdf_python_proto/legacy_grpc/__init__.py +1 -0
  79. otdf_python_proto/legacy_grpc/authorization/authorization_pb2_grpc.py +163 -0
  80. otdf_python_proto/legacy_grpc/authorization/v2/authorization_pb2_grpc.py +206 -0
  81. otdf_python_proto/legacy_grpc/common/common_pb2_grpc.py +4 -0
  82. otdf_python_proto/legacy_grpc/entity/entity_pb2_grpc.py +4 -0
  83. otdf_python_proto/legacy_grpc/entityresolution/entity_resolution_pb2_grpc.py +122 -0
  84. otdf_python_proto/legacy_grpc/entityresolution/v2/entity_resolution_pb2_grpc.py +120 -0
  85. otdf_python_proto/legacy_grpc/kas/kas_pb2_grpc.py +172 -0
  86. otdf_python_proto/legacy_grpc/logger/audit/test_pb2_grpc.py +4 -0
  87. otdf_python_proto/legacy_grpc/policy/actions/actions_pb2_grpc.py +249 -0
  88. otdf_python_proto/legacy_grpc/policy/attributes/attributes_pb2_grpc.py +873 -0
  89. otdf_python_proto/legacy_grpc/policy/kasregistry/key_access_server_registry_pb2_grpc.py +602 -0
  90. otdf_python_proto/legacy_grpc/policy/keymanagement/key_management_pb2_grpc.py +251 -0
  91. otdf_python_proto/legacy_grpc/policy/namespaces/namespaces_pb2_grpc.py +427 -0
  92. otdf_python_proto/legacy_grpc/policy/objects_pb2_grpc.py +4 -0
  93. otdf_python_proto/legacy_grpc/policy/registeredresources/registered_resources_pb2_grpc.py +524 -0
  94. otdf_python_proto/legacy_grpc/policy/resourcemapping/resource_mapping_pb2_grpc.py +516 -0
  95. otdf_python_proto/legacy_grpc/policy/selectors_pb2_grpc.py +4 -0
  96. otdf_python_proto/legacy_grpc/policy/subjectmapping/subject_mapping_pb2_grpc.py +551 -0
  97. otdf_python_proto/legacy_grpc/policy/unsafe/unsafe_pb2_grpc.py +485 -0
  98. otdf_python_proto/legacy_grpc/wellknownconfiguration/wellknown_configuration_pb2_grpc.py +77 -0
  99. otdf_python_proto/logger/__init__.py +1 -0
  100. otdf_python_proto/logger/audit/test_pb2.py +43 -0
  101. otdf_python_proto/logger/audit/test_pb2.pyi +45 -0
  102. otdf_python_proto/policy/__init__.py +1 -0
  103. otdf_python_proto/policy/actions/actions_pb2.py +75 -0
  104. otdf_python_proto/policy/actions/actions_pb2.pyi +87 -0
  105. otdf_python_proto/policy/actions/actions_pb2_connect.py +275 -0
  106. otdf_python_proto/policy/attributes/attributes_pb2.py +234 -0
  107. otdf_python_proto/policy/attributes/attributes_pb2.pyi +328 -0
  108. otdf_python_proto/policy/attributes/attributes_pb2_connect.py +863 -0
  109. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.py +266 -0
  110. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.pyi +450 -0
  111. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +611 -0
  112. otdf_python_proto/policy/keymanagement/key_management_pb2.py +79 -0
  113. otdf_python_proto/policy/keymanagement/key_management_pb2.pyi +87 -0
  114. otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +275 -0
  115. otdf_python_proto/policy/namespaces/namespaces_pb2.py +117 -0
  116. otdf_python_proto/policy/namespaces/namespaces_pb2.pyi +147 -0
  117. otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +443 -0
  118. otdf_python_proto/policy/objects_pb2.py +150 -0
  119. otdf_python_proto/policy/objects_pb2.pyi +464 -0
  120. otdf_python_proto/policy/registeredresources/registered_resources_pb2.py +139 -0
  121. otdf_python_proto/policy/registeredresources/registered_resources_pb2.pyi +196 -0
  122. otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +527 -0
  123. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.py +139 -0
  124. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.pyi +194 -0
  125. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +527 -0
  126. otdf_python_proto/policy/selectors_pb2.py +57 -0
  127. otdf_python_proto/policy/selectors_pb2.pyi +90 -0
  128. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.py +127 -0
  129. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.pyi +189 -0
  130. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +569 -0
  131. otdf_python_proto/policy/unsafe/unsafe_pb2.py +113 -0
  132. otdf_python_proto/policy/unsafe/unsafe_pb2.pyi +145 -0
  133. otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +485 -0
  134. otdf_python_proto/wellknownconfiguration/__init__.py +1 -0
  135. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py +51 -0
  136. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi +32 -0
  137. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +107 -0
  138. otdf_python/_gotdf_python.cpython-312-darwin.so +0 -0
  139. otdf_python/build.py +0 -190
  140. otdf_python/go.py +0 -1478
  141. otdf_python/gotdf_python.py +0 -383
  142. otdf_python-0.1.10.dist-info/METADATA +0 -149
  143. otdf_python-0.1.10.dist-info/RECORD +0 -10
  144. 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()
@@ -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,85 @@
1
+ """
2
+ Asymmetric encryption and decryption utilities for RSA keys in PEM format.
3
+ """
4
+
5
+ from cryptography.hazmat.backends import default_backend
6
+ from cryptography.hazmat.primitives import hashes, serialization
7
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
8
+ from cryptography.x509 import load_pem_x509_certificate
9
+
10
+ from .sdk_exceptions import SDKException
11
+
12
+
13
+ class AsymDecryption:
14
+ """
15
+ Provides functionality for asymmetric decryption using an RSA private key.
16
+ """
17
+
18
+ def __init__(self, private_key_pem: str):
19
+ try:
20
+ self.private_key = serialization.load_pem_private_key(
21
+ private_key_pem.encode(), password=None, backend=default_backend()
22
+ )
23
+ except Exception as e:
24
+ raise SDKException(f"Failed to load private key: {e}")
25
+
26
+ def decrypt(self, data: bytes) -> bytes:
27
+ if not self.private_key:
28
+ raise SDKException("Failed to decrypt, private key is empty")
29
+ try:
30
+ return self.private_key.decrypt(
31
+ data,
32
+ padding.OAEP(
33
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
34
+ algorithm=hashes.SHA1(),
35
+ label=None,
36
+ ),
37
+ )
38
+ except Exception as e:
39
+ raise SDKException(f"Error performing decryption: {e}")
40
+
41
+
42
+ class AsymEncryption:
43
+ """
44
+ Provides functionality for asymmetric encryption using an RSA public key or certificate in PEM format.
45
+ """
46
+
47
+ def __init__(self, public_key_pem: str):
48
+ try:
49
+ if "BEGIN CERTIFICATE" in public_key_pem:
50
+ cert = load_pem_x509_certificate(
51
+ public_key_pem.encode(), default_backend()
52
+ )
53
+ self.public_key = cert.public_key()
54
+ else:
55
+ self.public_key = serialization.load_pem_public_key(
56
+ public_key_pem.encode(), backend=default_backend()
57
+ )
58
+ except Exception as e:
59
+ raise SDKException(f"Failed to load public key: {e}")
60
+
61
+ if not isinstance(self.public_key, rsa.RSAPublicKey):
62
+ raise SDKException("Not an RSA PEM formatted public key")
63
+
64
+ def encrypt(self, data: bytes) -> bytes:
65
+ try:
66
+ return self.public_key.encrypt(
67
+ data,
68
+ padding.OAEP(
69
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
70
+ algorithm=hashes.SHA1(),
71
+ label=None,
72
+ ),
73
+ )
74
+ except Exception as e:
75
+ raise SDKException(f"Error performing encryption: {e}")
76
+
77
+ def public_key_in_pem_format(self) -> str:
78
+ try:
79
+ pem = self.public_key.public_bytes(
80
+ encoding=serialization.Encoding.PEM,
81
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
82
+ )
83
+ return pem.decode()
84
+ except Exception as e:
85
+ raise SDKException(f"Error exporting public key to PEM: {e}")
@@ -0,0 +1,53 @@
1
+ import base64
2
+
3
+ from cryptography.hazmat.backends import default_backend
4
+ from cryptography.hazmat.primitives import hashes, serialization
5
+ from cryptography.hazmat.primitives.asymmetric import padding
6
+
7
+ from .sdk_exceptions import SDKException
8
+
9
+
10
+ class AsymDecryption:
11
+ """
12
+ Class providing functionality for asymmetric decryption using an RSA private key.
13
+ """
14
+
15
+ CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
16
+ PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"
17
+ PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----"
18
+
19
+ def __init__(self, private_key_pem: str | None = None, private_key_obj=None):
20
+ if private_key_obj is not None:
21
+ self.private_key = private_key_obj
22
+ elif private_key_pem is not None:
23
+ try:
24
+ private_key_pem = (
25
+ private_key_pem.replace(self.PRIVATE_KEY_HEADER, "")
26
+ .replace(self.PRIVATE_KEY_FOOTER, "")
27
+ .replace("\n", "")
28
+ .replace("\r", "")
29
+ .replace(" ", "")
30
+ )
31
+ decoded = base64.b64decode(private_key_pem)
32
+ self.private_key = serialization.load_der_private_key(
33
+ decoded, password=None, backend=default_backend()
34
+ )
35
+ except Exception as e:
36
+ raise SDKException(f"Failed to load private key: {e}")
37
+ else:
38
+ self.private_key = None
39
+
40
+ def decrypt(self, data: bytes) -> bytes:
41
+ if self.private_key is None:
42
+ raise SDKException("Failed to decrypt, private key is empty")
43
+ try:
44
+ return self.private_key.decrypt(
45
+ data,
46
+ padding.OAEP(
47
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
48
+ algorithm=hashes.SHA1(),
49
+ label=None,
50
+ ),
51
+ )
52
+ except Exception as e:
53
+ raise SDKException(f"Error performing decryption: {e}")
@@ -0,0 +1,75 @@
1
+ import base64
2
+ import re
3
+
4
+ from cryptography.hazmat.backends import default_backend
5
+ from cryptography.hazmat.primitives import hashes, serialization
6
+ from cryptography.hazmat.primitives.asymmetric import padding
7
+ from cryptography.x509 import load_pem_x509_certificate
8
+
9
+ from .sdk_exceptions import SDKException
10
+
11
+
12
+ class AsymEncryption:
13
+ """
14
+ Provides methods for asymmetric encryption and handling public keys in PEM format.
15
+ """
16
+
17
+ PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----"
18
+ PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----"
19
+ CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
20
+
21
+ def __init__(self, public_key_pem: str | None = None, public_key_obj=None):
22
+ if public_key_obj is not None:
23
+ self.public_key = public_key_obj
24
+ elif public_key_pem is not None:
25
+ try:
26
+ if "BEGIN CERTIFICATE" in public_key_pem:
27
+ cert = load_pem_x509_certificate(
28
+ public_key_pem.encode(), default_backend()
29
+ )
30
+ self.public_key = cert.public_key()
31
+ else:
32
+ # Remove PEM headers/footers and whitespace
33
+ pem_body = re.sub(r"-----BEGIN (.*)-----", "", public_key_pem)
34
+ pem_body = re.sub(r"-----END (.*)-----", "", pem_body)
35
+ pem_body = re.sub(r"\s", "", pem_body)
36
+ decoded = base64.b64decode(pem_body)
37
+ self.public_key = serialization.load_der_public_key(
38
+ decoded, backend=default_backend()
39
+ )
40
+ except Exception as e:
41
+ raise SDKException(f"Failed to load public key: {e}")
42
+ else:
43
+ self.public_key = None
44
+
45
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
46
+
47
+ if self.public_key is not None and not isinstance(
48
+ self.public_key, RSAPublicKey
49
+ ):
50
+ raise SDKException("Not an RSA PEM formatted public key")
51
+
52
+ def encrypt(self, data: bytes) -> bytes:
53
+ if self.public_key is None:
54
+ raise SDKException("Failed to encrypt, public key is empty")
55
+ try:
56
+ return self.public_key.encrypt(
57
+ data,
58
+ padding.OAEP(
59
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
60
+ algorithm=hashes.SHA1(),
61
+ label=None,
62
+ ),
63
+ )
64
+ except Exception as e:
65
+ raise SDKException(f"Error performing encryption: {e}")
66
+
67
+ def public_key_in_pem_format(self) -> str:
68
+ try:
69
+ pem = self.public_key.public_bytes(
70
+ encoding=serialization.Encoding.PEM,
71
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
72
+ )
73
+ return pem.decode()
74
+ except Exception as e:
75
+ raise SDKException(f"Error exporting public key to PEM: {e}")
@@ -0,0 +1,21 @@
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
@@ -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)