otdf-python 0.4.0__py3-none-any.whl → 0.4.2__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 +1 -2
- otdf_python/__main__.py +1 -2
- otdf_python/address_normalizer.py +8 -10
- otdf_python/aesgcm.py +8 -0
- otdf_python/assertion_config.py +21 -0
- otdf_python/asym_crypto.py +18 -22
- otdf_python/auth_headers.py +7 -6
- otdf_python/autoconfigure_utils.py +21 -7
- otdf_python/cli.py +5 -5
- otdf_python/collection_store.py +13 -1
- otdf_python/collection_store_impl.py +5 -0
- otdf_python/config.py +13 -0
- otdf_python/connect_client.py +1 -0
- otdf_python/constants.py +2 -0
- otdf_python/crypto_utils.py +4 -0
- otdf_python/dpop.py +3 -5
- otdf_python/ecc_constants.py +12 -14
- otdf_python/ecc_mode.py +7 -2
- otdf_python/ecdh.py +24 -31
- otdf_python/eckeypair.py +5 -0
- otdf_python/header.py +5 -0
- otdf_python/invalid_zip_exception.py +6 -2
- otdf_python/kas_client.py +66 -55
- otdf_python/kas_connect_rpc_client.py +75 -38
- otdf_python/kas_info.py +4 -3
- otdf_python/kas_key_cache.py +10 -9
- otdf_python/key_type.py +4 -0
- otdf_python/key_type_constants.py +4 -11
- otdf_python/manifest.py +24 -0
- otdf_python/nanotdf.py +30 -28
- otdf_python/nanotdf_ecdsa_struct.py +5 -11
- otdf_python/nanotdf_type.py +13 -1
- otdf_python/policy_binding_serializer.py +6 -4
- otdf_python/policy_info.py +6 -0
- otdf_python/policy_object.py +8 -0
- otdf_python/policy_stub.py +2 -0
- otdf_python/resource_locator.py +22 -13
- otdf_python/sdk.py +51 -73
- otdf_python/sdk_builder.py +60 -47
- otdf_python/sdk_exceptions.py +11 -1
- otdf_python/symmetric_and_payload_config.py +6 -0
- otdf_python/tdf.py +47 -10
- otdf_python/tdf_reader.py +10 -13
- otdf_python/tdf_writer.py +5 -0
- otdf_python/token_source.py +4 -3
- otdf_python/version.py +5 -0
- otdf_python/zip_reader.py +10 -2
- otdf_python/zip_writer.py +11 -0
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/METADATA +3 -2
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/RECORD +81 -72
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/WHEEL +1 -1
- otdf_python_proto/__init__.py +2 -6
- otdf_python_proto/authorization/__init__.py +10 -0
- otdf_python_proto/authorization/authorization_connect.py +250 -0
- otdf_python_proto/authorization/v2/authorization_connect.py +315 -0
- otdf_python_proto/entityresolution/__init__.py +10 -0
- otdf_python_proto/entityresolution/entity_resolution_connect.py +185 -0
- otdf_python_proto/entityresolution/v2/entity_resolution_connect.py +185 -0
- otdf_python_proto/kas/__init__.py +2 -2
- otdf_python_proto/kas/kas_connect.py +259 -0
- otdf_python_proto/policy/actions/__init__.py +11 -0
- otdf_python_proto/policy/actions/actions_connect.py +380 -0
- otdf_python_proto/policy/attributes/__init__.py +11 -0
- otdf_python_proto/policy/attributes/attributes_connect.py +1310 -0
- otdf_python_proto/policy/kasregistry/__init__.py +11 -0
- otdf_python_proto/policy/kasregistry/key_access_server_registry_connect.py +912 -0
- otdf_python_proto/policy/keymanagement/__init__.py +11 -0
- otdf_python_proto/policy/keymanagement/key_management_connect.py +380 -0
- otdf_python_proto/policy/namespaces/__init__.py +11 -0
- otdf_python_proto/policy/namespaces/namespaces_connect.py +648 -0
- otdf_python_proto/policy/registeredresources/__init__.py +11 -0
- otdf_python_proto/policy/registeredresources/registered_resources_connect.py +770 -0
- otdf_python_proto/policy/resourcemapping/__init__.py +11 -0
- otdf_python_proto/policy/resourcemapping/resource_mapping_connect.py +790 -0
- otdf_python_proto/policy/subjectmapping/__init__.py +11 -0
- otdf_python_proto/policy/subjectmapping/subject_mapping_connect.py +851 -0
- otdf_python_proto/policy/unsafe/__init__.py +11 -0
- otdf_python_proto/policy/unsafe/unsafe_connect.py +705 -0
- otdf_python_proto/wellknownconfiguration/__init__.py +10 -0
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_connect.py +124 -0
- otdf_python_proto/authorization/authorization_pb2_connect.py +0 -191
- otdf_python_proto/authorization/v2/authorization_pb2_connect.py +0 -233
- otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +0 -149
- otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +0 -149
- otdf_python_proto/kas/kas_pb2_connect.py +0 -192
- otdf_python_proto/policy/actions/actions_pb2_connect.py +0 -275
- otdf_python_proto/policy/attributes/attributes_pb2_connect.py +0 -863
- otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +0 -611
- otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +0 -275
- otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +0 -443
- otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +0 -527
- otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +0 -527
- otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +0 -569
- otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +0 -485
- otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +0 -107
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.2.dist-info}/licenses/LICENSE +0 -0
otdf_python/kas_key_cache.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"""
|
|
2
|
-
KASKeyCache: In-memory cache for KAS (Key Access Service) public keys and info.
|
|
3
|
-
"""
|
|
1
|
+
"""KASKeyCache: In-memory cache for KAS (Key Access Service) public keys and info."""
|
|
4
2
|
|
|
5
3
|
import threading
|
|
6
4
|
from typing import Any
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class KASKeyCache:
|
|
8
|
+
"""In-memory cache for KAS public keys and information."""
|
|
9
|
+
|
|
10
10
|
def __init__(self):
|
|
11
|
+
"""Initialize KAS key cache."""
|
|
11
12
|
self._cache = {}
|
|
12
13
|
self._lock = threading.Lock()
|
|
13
14
|
|
|
14
15
|
def get(self, url: str, algorithm: str | None = None) -> Any | None:
|
|
15
|
-
"""
|
|
16
|
-
Gets a KASInfo object from the cache based on URL and algorithm.
|
|
16
|
+
"""Get a KASInfo object from cache based on URL and algorithm.
|
|
17
17
|
|
|
18
18
|
Args:
|
|
19
19
|
url: The URL of the KAS
|
|
@@ -21,17 +21,18 @@ class KASKeyCache:
|
|
|
21
21
|
|
|
22
22
|
Returns:
|
|
23
23
|
The cached KASInfo object, or None if not found
|
|
24
|
+
|
|
24
25
|
"""
|
|
25
26
|
cache_key = self._make_key(url, algorithm)
|
|
26
27
|
with self._lock:
|
|
27
28
|
return self._cache.get(cache_key)
|
|
28
29
|
|
|
29
30
|
def store(self, kas_info) -> None:
|
|
30
|
-
"""
|
|
31
|
-
Stores a KASInfo object in the cache.
|
|
31
|
+
"""Store a KASInfo object in cache.
|
|
32
32
|
|
|
33
33
|
Args:
|
|
34
34
|
kas_info: The KASInfo object to store
|
|
35
|
+
|
|
35
36
|
"""
|
|
36
37
|
cache_key = self._make_key(kas_info.url, getattr(kas_info, "algorithm", None))
|
|
37
38
|
with self._lock:
|
|
@@ -43,10 +44,10 @@ class KASKeyCache:
|
|
|
43
44
|
self._cache[key] = value
|
|
44
45
|
|
|
45
46
|
def clear(self):
|
|
46
|
-
"""
|
|
47
|
+
"""Clear the cache."""
|
|
47
48
|
with self._lock:
|
|
48
49
|
self._cache.clear()
|
|
49
50
|
|
|
50
51
|
def _make_key(self, url: str, algorithm: str | None = None) -> str:
|
|
51
|
-
"""
|
|
52
|
+
"""Create a cache key from URL and algorithm."""
|
|
52
53
|
return f"{url}:{algorithm or ''}"
|
otdf_python/key_type.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Constants for session key types used in the KAS client.
|
|
1
|
+
"""Constants for session key types used in the KAS client.
|
|
3
2
|
This matches the Java SDK's KeyType enum pattern.
|
|
4
3
|
"""
|
|
5
4
|
|
|
@@ -7,9 +6,7 @@ from enum import Enum, auto
|
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class KeyType(Enum):
|
|
10
|
-
"""
|
|
11
|
-
Enum for key types used in the KAS client.
|
|
12
|
-
"""
|
|
9
|
+
"""Enum for key types used in the KAS client."""
|
|
13
10
|
|
|
14
11
|
RSA2048 = auto()
|
|
15
12
|
EC_P256 = auto()
|
|
@@ -18,16 +15,12 @@ class KeyType(Enum):
|
|
|
18
15
|
|
|
19
16
|
@property
|
|
20
17
|
def is_ec(self):
|
|
21
|
-
"""
|
|
22
|
-
Returns True if this key type is an EC key, False otherwise.
|
|
23
|
-
"""
|
|
18
|
+
"""Returns True if this key type is an EC key, False otherwise."""
|
|
24
19
|
return self in [KeyType.EC_P256, KeyType.EC_P384, KeyType.EC_P521]
|
|
25
20
|
|
|
26
21
|
@property
|
|
27
22
|
def curve_name(self):
|
|
28
|
-
"""
|
|
29
|
-
Returns the curve name for EC keys.
|
|
30
|
-
"""
|
|
23
|
+
"""Returns the curve name for EC keys."""
|
|
31
24
|
if self == KeyType.EC_P256:
|
|
32
25
|
return "P-256"
|
|
33
26
|
elif self == KeyType.EC_P384:
|
otdf_python/manifest.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""TDF manifest representation and serialization."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
from dataclasses import asdict, dataclass, field
|
|
3
5
|
from typing import Any
|
|
@@ -5,6 +7,8 @@ from typing import Any
|
|
|
5
7
|
|
|
6
8
|
@dataclass
|
|
7
9
|
class ManifestSegment:
|
|
10
|
+
"""Encrypted segment information in TDF manifest."""
|
|
11
|
+
|
|
8
12
|
hash: str
|
|
9
13
|
segmentSize: int
|
|
10
14
|
encryptedSegmentSize: int
|
|
@@ -12,12 +16,16 @@ class ManifestSegment:
|
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
18
|
class ManifestRootSignature:
|
|
19
|
+
"""Root signature for manifest integrity."""
|
|
20
|
+
|
|
15
21
|
alg: str
|
|
16
22
|
sig: str
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
@dataclass
|
|
20
26
|
class ManifestIntegrityInformation:
|
|
27
|
+
"""Manifest integrity information with signatures and hashes."""
|
|
28
|
+
|
|
21
29
|
rootSignature: ManifestRootSignature
|
|
22
30
|
segmentHashAlg: str
|
|
23
31
|
segmentSizeDefault: int
|
|
@@ -27,12 +35,16 @@ class ManifestIntegrityInformation:
|
|
|
27
35
|
|
|
28
36
|
@dataclass
|
|
29
37
|
class ManifestPolicyBinding:
|
|
38
|
+
"""Policy binding with algorithm and hash."""
|
|
39
|
+
|
|
30
40
|
alg: str
|
|
31
41
|
hash: str
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
@dataclass
|
|
35
45
|
class ManifestKeyAccess:
|
|
46
|
+
"""Key access information in manifest."""
|
|
47
|
+
|
|
36
48
|
type: str
|
|
37
49
|
url: str
|
|
38
50
|
protocol: str
|
|
@@ -47,6 +59,8 @@ class ManifestKeyAccess:
|
|
|
47
59
|
|
|
48
60
|
@dataclass
|
|
49
61
|
class ManifestMethod:
|
|
62
|
+
"""Encryption method information in manifest."""
|
|
63
|
+
|
|
50
64
|
algorithm: str
|
|
51
65
|
iv: str
|
|
52
66
|
isStreamable: bool | None = None
|
|
@@ -54,6 +68,8 @@ class ManifestMethod:
|
|
|
54
68
|
|
|
55
69
|
@dataclass
|
|
56
70
|
class ManifestEncryptionInformation:
|
|
71
|
+
"""Encryption information in TDF manifest."""
|
|
72
|
+
|
|
57
73
|
type: str
|
|
58
74
|
policy: str
|
|
59
75
|
keyAccess: list[ManifestKeyAccess]
|
|
@@ -63,6 +79,8 @@ class ManifestEncryptionInformation:
|
|
|
63
79
|
|
|
64
80
|
@dataclass
|
|
65
81
|
class ManifestPayload:
|
|
82
|
+
"""Payload information in TDF manifest."""
|
|
83
|
+
|
|
66
84
|
type: str
|
|
67
85
|
url: str
|
|
68
86
|
protocol: str
|
|
@@ -72,12 +90,16 @@ class ManifestPayload:
|
|
|
72
90
|
|
|
73
91
|
@dataclass
|
|
74
92
|
class ManifestBinding:
|
|
93
|
+
"""Assertion binding information."""
|
|
94
|
+
|
|
75
95
|
method: str
|
|
76
96
|
signature: str
|
|
77
97
|
|
|
78
98
|
|
|
79
99
|
@dataclass
|
|
80
100
|
class ManifestAssertion:
|
|
101
|
+
"""TDF assertion in manifest."""
|
|
102
|
+
|
|
81
103
|
id: str
|
|
82
104
|
type: str
|
|
83
105
|
scope: str
|
|
@@ -88,6 +110,8 @@ class ManifestAssertion:
|
|
|
88
110
|
|
|
89
111
|
@dataclass
|
|
90
112
|
class Manifest:
|
|
113
|
+
"""TDF manifest with encryption and payload information."""
|
|
114
|
+
|
|
91
115
|
schemaVersion: str | None = None
|
|
92
116
|
encryptionInformation: ManifestEncryptionInformation | None = None
|
|
93
117
|
payload: ManifestPayload | None = None
|
otdf_python/nanotdf.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""NanoTDF reader and writer implementation."""
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
4
|
import hashlib
|
|
3
5
|
import json
|
|
@@ -24,22 +26,24 @@ from .asym_crypto import AsymDecryption
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class NanoTDFException(SDKException):
|
|
27
|
-
|
|
29
|
+
"""Base exception for NanoTDF operations."""
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class NanoTDFMaxSizeLimit(NanoTDFException):
|
|
31
|
-
|
|
33
|
+
"""Exception for NanoTDF size limit exceeded."""
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class UnsupportedNanoTDFFeature(NanoTDFException):
|
|
35
|
-
|
|
37
|
+
"""Exception for unsupported NanoTDF features."""
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class InvalidNanoTDFConfig(NanoTDFException):
|
|
39
|
-
|
|
41
|
+
"""Exception for invalid NanoTDF configuration."""
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class NanoTDF:
|
|
45
|
+
"""NanoTDF reader and writer for compact TDF format."""
|
|
46
|
+
|
|
43
47
|
MAGIC_NUMBER_AND_VERSION = MAGIC_NUMBER_AND_VERSION
|
|
44
48
|
K_MAX_TDF_SIZE = (16 * 1024 * 1024) - 3 - 32
|
|
45
49
|
K_NANOTDF_GMAC_LENGTH = 8
|
|
@@ -48,6 +52,7 @@ class NanoTDF:
|
|
|
48
52
|
K_EMPTY_IV = bytes([0x0] * 12)
|
|
49
53
|
|
|
50
54
|
def __init__(self, services=None, collection_store: CollectionStore | None = None):
|
|
55
|
+
"""Initialize NanoTDF reader/writer."""
|
|
51
56
|
self.services = services
|
|
52
57
|
self.collection_store = collection_store or NoOpCollectionStore()
|
|
53
58
|
|
|
@@ -59,7 +64,7 @@ class NanoTDF:
|
|
|
59
64
|
return PolicyObject(uuid=policy_uuid, body=body)
|
|
60
65
|
|
|
61
66
|
def _serialize_policy_object(self, obj):
|
|
62
|
-
"""
|
|
67
|
+
"""Serialize policy object to compatible JSON format."""
|
|
63
68
|
from otdf_python.policy_object import AttributeObject, PolicyBody
|
|
64
69
|
|
|
65
70
|
if isinstance(obj, PolicyBody):
|
|
@@ -82,8 +87,7 @@ class NanoTDF:
|
|
|
82
87
|
return obj.__dict__
|
|
83
88
|
|
|
84
89
|
def _prepare_payload(self, payload: bytes | BytesIO) -> bytes:
|
|
85
|
-
"""
|
|
86
|
-
Convert BytesIO to bytes and validate payload size.
|
|
90
|
+
"""Convert BytesIO to bytes and validate payload size.
|
|
87
91
|
|
|
88
92
|
Args:
|
|
89
93
|
payload: The payload data as bytes or BytesIO
|
|
@@ -93,6 +97,7 @@ class NanoTDF:
|
|
|
93
97
|
|
|
94
98
|
Raises:
|
|
95
99
|
NanoTDFMaxSizeLimit: If the payload exceeds the maximum size
|
|
100
|
+
|
|
96
101
|
"""
|
|
97
102
|
if isinstance(payload, BytesIO):
|
|
98
103
|
payload = payload.getvalue()
|
|
@@ -101,14 +106,14 @@ class NanoTDF:
|
|
|
101
106
|
return payload
|
|
102
107
|
|
|
103
108
|
def _prepare_policy_data(self, config: NanoTDFConfig) -> tuple[bytes, str]:
|
|
104
|
-
"""
|
|
105
|
-
Prepare policy data from configuration.
|
|
109
|
+
"""Prepare policy data from configuration.
|
|
106
110
|
|
|
107
111
|
Args:
|
|
108
112
|
config: NanoTDFConfig configuration
|
|
109
113
|
|
|
110
114
|
Returns:
|
|
111
115
|
tuple: (policy_body, policy_type)
|
|
116
|
+
|
|
112
117
|
"""
|
|
113
118
|
attributes = config.attributes if config.attributes else []
|
|
114
119
|
policy_object = self._create_policy_object(attributes)
|
|
@@ -150,8 +155,7 @@ class NanoTDF:
|
|
|
150
155
|
config: NanoTDFConfig,
|
|
151
156
|
ephemeral_public_key: bytes | None = None,
|
|
152
157
|
) -> bytes:
|
|
153
|
-
"""
|
|
154
|
-
Create the NanoTDF header.
|
|
158
|
+
"""Create the NanoTDF header.
|
|
155
159
|
|
|
156
160
|
Args:
|
|
157
161
|
policy_body: The policy body bytes
|
|
@@ -161,6 +165,7 @@ class NanoTDF:
|
|
|
161
165
|
|
|
162
166
|
Returns:
|
|
163
167
|
bytes: The header bytes
|
|
168
|
+
|
|
164
169
|
"""
|
|
165
170
|
from otdf_python.header import Header # Local import to avoid circular import
|
|
166
171
|
|
|
@@ -228,8 +233,7 @@ class NanoTDF:
|
|
|
228
233
|
return self.MAGIC_NUMBER_AND_VERSION + header_bytes
|
|
229
234
|
|
|
230
235
|
def _is_ec_key(self, key_pem: str) -> bool:
|
|
231
|
-
"""
|
|
232
|
-
Detect if a PEM key is an EC key (vs RSA).
|
|
236
|
+
"""Detect if a PEM key is an EC key (vs RSA).
|
|
233
237
|
|
|
234
238
|
Args:
|
|
235
239
|
key_pem: PEM-formatted key string
|
|
@@ -239,6 +243,7 @@ class NanoTDF:
|
|
|
239
243
|
|
|
240
244
|
Raises:
|
|
241
245
|
SDKException: If key cannot be parsed
|
|
246
|
+
|
|
242
247
|
"""
|
|
243
248
|
try:
|
|
244
249
|
# Try to load as public key first
|
|
@@ -260,13 +265,12 @@ class NanoTDF:
|
|
|
260
265
|
else:
|
|
261
266
|
raise SDKException("Invalid PEM format - no BEGIN header found")
|
|
262
267
|
except Exception as e:
|
|
263
|
-
raise SDKException(f"Failed to detect key type: {e}")
|
|
268
|
+
raise SDKException(f"Failed to detect key type: {e}") from e
|
|
264
269
|
|
|
265
270
|
def _derive_key_with_ecdh( # noqa: C901
|
|
266
271
|
self, config: NanoTDFConfig
|
|
267
272
|
) -> tuple[bytes, bytes | None, bytes | None]:
|
|
268
|
-
"""
|
|
269
|
-
Derive encryption key using ECDH if KAS public key is provided or can be fetched.
|
|
273
|
+
"""Derive encryption key using ECDH if KAS public key is provided or can be fetched.
|
|
270
274
|
|
|
271
275
|
This implements the NanoTDF spec's ECDH + HKDF key derivation:
|
|
272
276
|
1. Generate ephemeral keypair
|
|
@@ -283,6 +287,7 @@ class NanoTDF:
|
|
|
283
287
|
- derived_key: 32-byte AES-256 key for encrypting the payload
|
|
284
288
|
- ephemeral_public_key_compressed: Compressed ephemeral public key to store in header (None for RSA)
|
|
285
289
|
- kas_public_key: KAS public key PEM string (or None if not available)
|
|
290
|
+
|
|
286
291
|
"""
|
|
287
292
|
import logging
|
|
288
293
|
|
|
@@ -384,8 +389,7 @@ class NanoTDF:
|
|
|
384
389
|
return derived_key, ephemeral_public_key_compressed, kas_public_key
|
|
385
390
|
|
|
386
391
|
def _encrypt_payload(self, payload: bytes, key: bytes) -> tuple[bytes, bytes]:
|
|
387
|
-
"""
|
|
388
|
-
Encrypt the payload using AES-GCM.
|
|
392
|
+
"""Encrypt the payload using AES-GCM.
|
|
389
393
|
|
|
390
394
|
Args:
|
|
391
395
|
payload: The payload to encrypt
|
|
@@ -393,6 +397,7 @@ class NanoTDF:
|
|
|
393
397
|
|
|
394
398
|
Returns:
|
|
395
399
|
tuple: (iv, ciphertext)
|
|
400
|
+
|
|
396
401
|
"""
|
|
397
402
|
iv = secrets.token_bytes(self.K_NANOTDF_IV_SIZE)
|
|
398
403
|
iv_padded = self.K_EMPTY_IV[: self.K_IV_PADDING] + iv
|
|
@@ -403,8 +408,7 @@ class NanoTDF:
|
|
|
403
408
|
def create_nano_tdf(
|
|
404
409
|
self, payload: bytes | BytesIO, output_stream: BinaryIO, config: NanoTDFConfig
|
|
405
410
|
) -> int:
|
|
406
|
-
"""
|
|
407
|
-
Stream-based NanoTDF creation - writes encrypted payload to an output stream.
|
|
411
|
+
"""Stream-based NanoTDF creation - writes encrypted payload to an output stream.
|
|
408
412
|
|
|
409
413
|
For convenience method that returns bytes, use create_nanotdf() instead.
|
|
410
414
|
Supports ECDH key derivation if KAS info with public key is provided in config.
|
|
@@ -422,8 +426,8 @@ class NanoTDF:
|
|
|
422
426
|
UnsupportedNanoTDFFeature: If an unsupported feature is requested
|
|
423
427
|
InvalidNanoTDFConfig: If the configuration is invalid
|
|
424
428
|
SDKException: For other errors
|
|
425
|
-
"""
|
|
426
429
|
|
|
430
|
+
"""
|
|
427
431
|
# Process payload and validate size
|
|
428
432
|
payload = self._prepare_payload(payload)
|
|
429
433
|
|
|
@@ -557,8 +561,7 @@ class NanoTDF:
|
|
|
557
561
|
output_stream: BinaryIO,
|
|
558
562
|
config: NanoTDFConfig,
|
|
559
563
|
) -> None:
|
|
560
|
-
"""
|
|
561
|
-
Stream-based NanoTDF decryption - writes decrypted payload to an output stream.
|
|
564
|
+
"""Stream-based NanoTDF decryption - writes decrypted payload to an output stream.
|
|
562
565
|
|
|
563
566
|
For convenience method that returns bytes, use read_nanotdf() instead.
|
|
564
567
|
Supports ECDH key derivation and KAS key unwrapping.
|
|
@@ -571,6 +574,7 @@ class NanoTDF:
|
|
|
571
574
|
Raises:
|
|
572
575
|
InvalidNanoTDFConfig: If the NanoTDF format is invalid or config is missing required info
|
|
573
576
|
SDKException: For other errors
|
|
577
|
+
|
|
574
578
|
"""
|
|
575
579
|
# Convert to bytes if BytesIO
|
|
576
580
|
if isinstance(nano_tdf_data, BytesIO):
|
|
@@ -582,7 +586,7 @@ class NanoTDF:
|
|
|
582
586
|
header_len = Header.peek_length(nano_tdf_data)
|
|
583
587
|
header_obj = Header.from_bytes(nano_tdf_data[:header_len])
|
|
584
588
|
except Exception as e:
|
|
585
|
-
raise InvalidNanoTDFConfig(f"Failed to parse NanoTDF header: {e}")
|
|
589
|
+
raise InvalidNanoTDFConfig(f"Failed to parse NanoTDF header: {e}") from e
|
|
586
590
|
|
|
587
591
|
# Read payload section per NanoTDF spec:
|
|
588
592
|
# [3 bytes: length] [3 bytes: IV] [variable: ciphertext] [tag]
|
|
@@ -768,8 +772,7 @@ class NanoTDF:
|
|
|
768
772
|
return key, config
|
|
769
773
|
|
|
770
774
|
def create_nanotdf(self, data: bytes, config: dict | NanoTDFConfig) -> bytes:
|
|
771
|
-
"""
|
|
772
|
-
Convenience method - creates a NanoTDF and returns the encrypted bytes.
|
|
775
|
+
"""Create a NanoTDF and return the encrypted bytes.
|
|
773
776
|
|
|
774
777
|
For stream-based version, use create_nano_tdf() instead.
|
|
775
778
|
"""
|
|
@@ -846,8 +849,7 @@ class NanoTDF:
|
|
|
846
849
|
def read_nanotdf(
|
|
847
850
|
self, nanotdf_bytes: bytes, config: dict | NanoTDFConfig | None = None
|
|
848
851
|
) -> bytes:
|
|
849
|
-
"""
|
|
850
|
-
Convenience method - decrypts a NanoTDF and returns the plaintext bytes.
|
|
852
|
+
"""Decrypt a NanoTDF and return the plaintext bytes.
|
|
851
853
|
|
|
852
854
|
For stream-based version, use read_nano_tdf() instead.
|
|
853
855
|
"""
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
NanoTDF ECDSA Signature Structure.
|
|
3
|
-
"""
|
|
1
|
+
"""NanoTDF ECDSA Signature Structure."""
|
|
4
2
|
|
|
5
3
|
from dataclasses import dataclass, field
|
|
6
4
|
|
|
@@ -8,13 +6,10 @@ from dataclasses import dataclass, field
|
|
|
8
6
|
class IncorrectNanoTDFECDSASignatureSize(Exception):
|
|
9
7
|
"""Exception raised when the signature size is incorrect."""
|
|
10
8
|
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
9
|
|
|
14
10
|
@dataclass
|
|
15
11
|
class NanoTDFECDSAStruct:
|
|
16
|
-
"""
|
|
17
|
-
Class to handle ECDSA signature structure for NanoTDF.
|
|
12
|
+
"""Class to handle ECDSA signature structure for NanoTDF.
|
|
18
13
|
|
|
19
14
|
This structure represents an ECDSA signature as required by the NanoTDF format.
|
|
20
15
|
It consists of r and s values along with their lengths.
|
|
@@ -29,8 +24,7 @@ class NanoTDFECDSAStruct:
|
|
|
29
24
|
def from_bytes(
|
|
30
25
|
cls, ecdsa_signature_value: bytes, key_size: int
|
|
31
26
|
) -> "NanoTDFECDSAStruct":
|
|
32
|
-
"""
|
|
33
|
-
Create a NanoTDFECDSAStruct from a byte array.
|
|
27
|
+
"""Create a NanoTDFECDSAStruct from a byte array.
|
|
34
28
|
|
|
35
29
|
Args:
|
|
36
30
|
ecdsa_signature_value: The signature value as bytes
|
|
@@ -41,6 +35,7 @@ class NanoTDFECDSAStruct:
|
|
|
41
35
|
|
|
42
36
|
Raises:
|
|
43
37
|
IncorrectNanoTDFECDSASignatureSize: If the signature buffer size is invalid
|
|
38
|
+
|
|
44
39
|
"""
|
|
45
40
|
if len(ecdsa_signature_value) != (2 * key_size) + 2:
|
|
46
41
|
raise IncorrectNanoTDFECDSASignatureSize(
|
|
@@ -72,8 +67,7 @@ class NanoTDFECDSAStruct:
|
|
|
72
67
|
return struct_obj
|
|
73
68
|
|
|
74
69
|
def as_bytes(self) -> bytes:
|
|
75
|
-
"""
|
|
76
|
-
Convert the signature structure to bytes.
|
|
70
|
+
"""Convert the signature structure to bytes.
|
|
77
71
|
Raises ValueError if r_value or s_value is None.
|
|
78
72
|
"""
|
|
79
73
|
if self.r_value is None or self.s_value is None:
|
otdf_python/nanotdf_type.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
"""NanoTDF type enumeration."""
|
|
2
|
+
|
|
1
3
|
from enum import Enum
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class ECCurve(Enum):
|
|
7
|
+
"""Elliptic curve enumeration for NanoTDF."""
|
|
8
|
+
|
|
5
9
|
SECP256R1 = "secp256r1"
|
|
6
10
|
SECP384R1 = "secp384r1"
|
|
7
|
-
SECP521R1 = "
|
|
11
|
+
SECP521R1 = "secp521r1"
|
|
8
12
|
SECP256K1 = "secp256k1"
|
|
9
13
|
|
|
10
14
|
def __str__(self):
|
|
@@ -12,11 +16,15 @@ class ECCurve(Enum):
|
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class Protocol(Enum):
|
|
19
|
+
"""Protocol enumeration for KAS communication."""
|
|
20
|
+
|
|
15
21
|
HTTP = "HTTP"
|
|
16
22
|
HTTPS = "HTTPS"
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
class IdentifierType(Enum):
|
|
26
|
+
"""Identifier type enumeration for NanoTDF."""
|
|
27
|
+
|
|
20
28
|
NONE = 0
|
|
21
29
|
TWO_BYTES = 2
|
|
22
30
|
EIGHT_BYTES = 8
|
|
@@ -27,6 +35,8 @@ class IdentifierType(Enum):
|
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
class PolicyType(Enum):
|
|
38
|
+
"""Policy type enumeration for NanoTDF."""
|
|
39
|
+
|
|
30
40
|
REMOTE_POLICY = 0
|
|
31
41
|
EMBEDDED_POLICY_PLAIN_TEXT = 1
|
|
32
42
|
EMBEDDED_POLICY_ENCRYPTED = 2
|
|
@@ -34,6 +44,8 @@ class PolicyType(Enum):
|
|
|
34
44
|
|
|
35
45
|
|
|
36
46
|
class Cipher(Enum):
|
|
47
|
+
"""Cipher enumeration for NanoTDF encryption."""
|
|
48
|
+
|
|
37
49
|
AES_256_GCM_64_TAG = 0
|
|
38
50
|
AES_256_GCM_96_TAG = 1
|
|
39
51
|
AES_256_GCM_104_TAG = 2
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
+
"""Policy binding serialization for HMAC calculation."""
|
|
2
|
+
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class PolicyBinding:
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
+
"""Represents a policy binding in the TDF manifest.
|
|
8
|
+
|
|
7
9
|
This is a placeholder implementation as the complete details of
|
|
8
10
|
the PolicyBinding class aren't provided in the code snippets.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
def __init__(self, **kwargs):
|
|
14
|
+
"""Initialize policy binding from kwargs."""
|
|
12
15
|
for key, value in kwargs.items():
|
|
13
16
|
setattr(self, key, value)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class PolicyBindingSerializer:
|
|
17
|
-
"""
|
|
18
|
-
Handles serialization and deserialization of policy bindings.
|
|
20
|
+
"""Handles serialization and deserialization of policy bindings.
|
|
19
21
|
This class provides static methods to convert between JSON representations
|
|
20
22
|
and PolicyBinding objects.
|
|
21
23
|
"""
|
otdf_python/policy_info.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
"""Policy information handling for NanoTDF."""
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class PolicyInfo:
|
|
5
|
+
"""Policy information."""
|
|
6
|
+
|
|
2
7
|
def __init__(
|
|
3
8
|
self,
|
|
4
9
|
policy_type: int = 0,
|
|
5
10
|
body: bytes | None = None,
|
|
6
11
|
):
|
|
12
|
+
"""Initialize policy information."""
|
|
7
13
|
self.policy_type = policy_type
|
|
8
14
|
self.body = body
|
|
9
15
|
|
otdf_python/policy_object.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
"""Policy object dataclasses for OpenTDF."""
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
@dataclass
|
|
5
7
|
class AttributeObject:
|
|
8
|
+
"""An attribute object."""
|
|
9
|
+
|
|
6
10
|
attribute: str
|
|
7
11
|
display_name: str | None = None
|
|
8
12
|
is_default: bool = False
|
|
@@ -12,11 +16,15 @@ class AttributeObject:
|
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
18
|
class PolicyBody:
|
|
19
|
+
"""A policy body."""
|
|
20
|
+
|
|
15
21
|
data_attributes: list[AttributeObject]
|
|
16
22
|
dissem: list[str]
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
@dataclass
|
|
20
26
|
class PolicyObject:
|
|
27
|
+
"""A policy object."""
|
|
28
|
+
|
|
21
29
|
uuid: str
|
|
22
30
|
body: PolicyBody
|
otdf_python/policy_stub.py
CHANGED
otdf_python/resource_locator.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
"""NanoTDF resource locator handling."""
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class ResourceLocator:
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
https://github.com/opentdf/spec/blob/main/schema/nanotdf/README.md
|
|
5
|
+
"""Represent NanoTDF Resource Locator per specification.
|
|
6
|
+
|
|
7
|
+
See https://github.com/opentdf/spec/blob/main/schema/nanotdf/README.md
|
|
5
8
|
|
|
6
9
|
Format:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
- Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
|
|
11
|
+
- Protocol: 0x0=HTTP, 0x1=HTTPS, 0xF=Shared Resource Directory
|
|
12
|
+
- Identifier: 0x0=None, 0x1=2 bytes, 0x2=8 bytes, 0x3=32 bytes
|
|
13
|
+
- Byte 1: Body Length (1-255 bytes)
|
|
14
|
+
- Bytes 2-N: Body (URL path)
|
|
15
|
+
- Bytes N+1-M: Identifier (optional, 0/2/8/32 bytes)
|
|
16
|
+
|
|
13
17
|
"""
|
|
14
18
|
|
|
15
19
|
# Protocol enum values
|
|
@@ -24,6 +28,13 @@ class ResourceLocator:
|
|
|
24
28
|
IDENTIFIER_32_BYTES = 0x3
|
|
25
29
|
|
|
26
30
|
def __init__(self, resource_url: str | None = None, identifier: str | None = None):
|
|
31
|
+
"""Initialize resource locator.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
resource_url: URL of the resource
|
|
35
|
+
identifier: Optional identifier for the resource
|
|
36
|
+
|
|
37
|
+
"""
|
|
27
38
|
self.resource_url = resource_url or ""
|
|
28
39
|
self.identifier = identifier or ""
|
|
29
40
|
|
|
@@ -71,8 +82,7 @@ class ResourceLocator:
|
|
|
71
82
|
raise ValueError(f"Identifier too long: {id_len} bytes (max 32)")
|
|
72
83
|
|
|
73
84
|
def to_bytes(self):
|
|
74
|
-
"""
|
|
75
|
-
Convert to NanoTDF Resource Locator format per spec.
|
|
85
|
+
"""Convert to NanoTDF Resource Locator format per spec.
|
|
76
86
|
|
|
77
87
|
Format:
|
|
78
88
|
- Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
|
|
@@ -106,8 +116,7 @@ class ResourceLocator:
|
|
|
106
116
|
|
|
107
117
|
@staticmethod
|
|
108
118
|
def from_bytes_with_size(buffer: bytes): # noqa: C901
|
|
109
|
-
"""
|
|
110
|
-
Parse NanoTDF Resource Locator from bytes per spec.
|
|
119
|
+
"""Parse NanoTDF Resource Locator from bytes per spec.
|
|
111
120
|
|
|
112
121
|
Format:
|
|
113
122
|
- Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
|