c2pie 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.
- c2pie/__init__.py +0 -0
- c2pie/c2pa/__init__.py +0 -0
- c2pie/c2pa/assertion.py +96 -0
- c2pie/c2pa/assertion_store.py +30 -0
- c2pie/c2pa/claim.py +136 -0
- c2pie/c2pa/claim_signature.py +101 -0
- c2pie/c2pa/config.py +1 -0
- c2pie/c2pa/manifest.py +56 -0
- c2pie/c2pa/manifest_store.py +34 -0
- c2pie/c2pa_injection/__init__.py +0 -0
- c2pie/c2pa_injection/jpg_injection.py +100 -0
- c2pie/c2pa_injection/pdf_injection.py +204 -0
- c2pie/interface.py +97 -0
- c2pie/jumbf_boxes/__init__.py +0 -0
- c2pie/jumbf_boxes/box.py +28 -0
- c2pie/jumbf_boxes/content_box.py +12 -0
- c2pie/jumbf_boxes/description_box.py +25 -0
- c2pie/jumbf_boxes/super_box.py +47 -0
- c2pie/main.py +70 -0
- c2pie/signing.py +229 -0
- c2pie/utils/__init__.py +0 -0
- c2pie/utils/assertion_schemas.py +54 -0
- c2pie/utils/content_types.py +27 -0
- c2pie-0.1.0.dist-info/METADATA +707 -0
- c2pie-0.1.0.dist-info/RECORD +28 -0
- c2pie-0.1.0.dist-info/WHEEL +4 -0
- c2pie-0.1.0.dist-info/entry_points.txt +3 -0
- c2pie-0.1.0.dist-info/licenses/LICENSE +201 -0
c2pie/__init__.py
ADDED
|
File without changes
|
c2pie/c2pa/__init__.py
ADDED
|
File without changes
|
c2pie/c2pa/assertion.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from c2pie.jumbf_boxes.content_box import ContentBox
|
|
6
|
+
from c2pie.jumbf_boxes.super_box import SuperBox
|
|
7
|
+
from c2pie.utils.assertion_schemas import (
|
|
8
|
+
C2PA_AssertionTypes,
|
|
9
|
+
cbor_to_bytes,
|
|
10
|
+
get_assertion_content_box_type,
|
|
11
|
+
get_assertion_content_type,
|
|
12
|
+
get_assertion_label,
|
|
13
|
+
json_to_bytes,
|
|
14
|
+
)
|
|
15
|
+
from c2pie.utils.content_types import jumbf_content_types
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Assertion(SuperBox):
|
|
19
|
+
"""Universal assertion superbox (one content box)."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
assertion_type: C2PA_AssertionTypes,
|
|
24
|
+
schema: dict[str, Any],
|
|
25
|
+
):
|
|
26
|
+
self.type = assertion_type
|
|
27
|
+
self.schema = schema
|
|
28
|
+
|
|
29
|
+
payload = self.get_payload_from_schema()
|
|
30
|
+
box_type_hex = get_assertion_content_box_type(self.type)
|
|
31
|
+
content_box = ContentBox(box_type=box_type_hex, payload=payload)
|
|
32
|
+
|
|
33
|
+
super().__init__(
|
|
34
|
+
content_type=get_assertion_content_type(self.type),
|
|
35
|
+
label=get_assertion_label(self.type),
|
|
36
|
+
content_boxes=[content_box],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def get_payload_from_schema(self) -> bytes:
|
|
40
|
+
ctype = get_assertion_content_type(self.type)
|
|
41
|
+
if ctype == jumbf_content_types["json"]:
|
|
42
|
+
return json_to_bytes(self.schema)
|
|
43
|
+
if ctype == jumbf_content_types["cbor"]:
|
|
44
|
+
return cbor_to_bytes(self.schema)
|
|
45
|
+
if ctype == jumbf_content_types["codestream"]:
|
|
46
|
+
return self.schema.get("payload", b"")
|
|
47
|
+
return b""
|
|
48
|
+
|
|
49
|
+
def get_data_for_signing(self) -> bytes:
|
|
50
|
+
return self.description_box.serialize() + self.serialize_content_boxes()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HashDataAssertion(Assertion):
|
|
54
|
+
"""c2pa.hash.data hard binding assertion."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
cai_offset: int,
|
|
59
|
+
hashed_data: bytes,
|
|
60
|
+
additional_exclusions: list[dict[str, int]] | None = None,
|
|
61
|
+
):
|
|
62
|
+
exclusions: list[dict[str, int]] = [{"start": cai_offset, "length": 65535}]
|
|
63
|
+
if additional_exclusions:
|
|
64
|
+
exclusions.extend(additional_exclusions)
|
|
65
|
+
|
|
66
|
+
schema: dict[str, Any] = {
|
|
67
|
+
"name": "jumbf manifest",
|
|
68
|
+
"exclusions": exclusions,
|
|
69
|
+
"alg": "sha256",
|
|
70
|
+
"hash": hashed_data,
|
|
71
|
+
"pad": [],
|
|
72
|
+
}
|
|
73
|
+
super().__init__(C2PA_AssertionTypes.data_hash, schema)
|
|
74
|
+
|
|
75
|
+
def set_hash_data_length(self, length: int) -> None:
|
|
76
|
+
if self.schema.get("name") != "jumbf manifest":
|
|
77
|
+
raise ValueError("c2pa.hash.data: jumbf manifest is missing")
|
|
78
|
+
exclusions = self.schema.get("exclusions", [])
|
|
79
|
+
if not exclusions:
|
|
80
|
+
raise ValueError("c2pa.hash.data: exclusions are missing")
|
|
81
|
+
exclusions[0]["length"] = int(length)
|
|
82
|
+
|
|
83
|
+
payload = self.get_payload_from_schema()
|
|
84
|
+
if self.content_boxes:
|
|
85
|
+
self.content_boxes[0] = ContentBox(
|
|
86
|
+
box_type=get_assertion_content_box_type(self.type),
|
|
87
|
+
payload=payload,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
self.content_boxes = [
|
|
91
|
+
ContentBox(
|
|
92
|
+
box_type=get_assertion_content_box_type(self.type),
|
|
93
|
+
payload=payload,
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
self.sync_payload()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from c2pie.jumbf_boxes.super_box import SuperBox
|
|
4
|
+
from c2pie.utils.assertion_schemas import C2PA_AssertionTypes
|
|
5
|
+
from c2pie.utils.content_types import c2pa_content_types
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AssertionStore(SuperBox):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
assertions: list,
|
|
12
|
+
):
|
|
13
|
+
self.assertions = assertions
|
|
14
|
+
super().__init__(
|
|
15
|
+
content_type=c2pa_content_types["assertions"],
|
|
16
|
+
label="c2pa.assertions",
|
|
17
|
+
content_boxes=self.assertions,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def get_assertions(self) -> list:
|
|
21
|
+
return self.assertions
|
|
22
|
+
|
|
23
|
+
def set_hash_data_length(
|
|
24
|
+
self,
|
|
25
|
+
length: int,
|
|
26
|
+
) -> None:
|
|
27
|
+
for assertion in self.assertions:
|
|
28
|
+
if assertion.type == C2PA_AssertionTypes.data_hash:
|
|
29
|
+
assertion.set_hash_data_length(length)
|
|
30
|
+
self.sync_payload()
|
c2pie/c2pa/claim.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import cbor2
|
|
8
|
+
|
|
9
|
+
from c2pie.c2pa.assertion_store import AssertionStore
|
|
10
|
+
from c2pie.jumbf_boxes.content_box import ContentBox
|
|
11
|
+
from c2pie.jumbf_boxes.super_box import SuperBox
|
|
12
|
+
from c2pie.utils.content_types import c2pa_content_types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sha256(b: bytes) -> bytes:
|
|
16
|
+
return hashlib.sha256(b).digest()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Claim(SuperBox):
|
|
20
|
+
"""Claim (c2pa.claim) as a JUMBF superbox with one CBOR content box."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
assertion_store: AssertionStore,
|
|
25
|
+
claim_generator: str = "c2pie",
|
|
26
|
+
manifest_label: str = f"urn:uuid:{uuid.uuid4().hex}",
|
|
27
|
+
dc_format: str = None,
|
|
28
|
+
):
|
|
29
|
+
self.claim_generator = claim_generator
|
|
30
|
+
self.manifest_label = manifest_label
|
|
31
|
+
self.assertion_store = assertion_store
|
|
32
|
+
self.dc_format = dc_format
|
|
33
|
+
|
|
34
|
+
self.claim_signature_label = f"self#jumbf=c2pa/{self.manifest_label}/c2pa.signature"
|
|
35
|
+
|
|
36
|
+
self._instance_id = f"xmp:iid:{uuid.uuid4()}"
|
|
37
|
+
|
|
38
|
+
cbor_payload = self._build_cbor_payload()
|
|
39
|
+
|
|
40
|
+
content_box = ContentBox(
|
|
41
|
+
box_type=b"cbor".hex(),
|
|
42
|
+
payload=cbor_payload,
|
|
43
|
+
)
|
|
44
|
+
super().__init__(
|
|
45
|
+
content_type=c2pa_content_types["claim"],
|
|
46
|
+
label="c2pa.claim",
|
|
47
|
+
content_boxes=[content_box],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def get_cbor_payload(self) -> bytes:
|
|
51
|
+
"""CBOR bytes, on top of which COSE (detached) is signed."""
|
|
52
|
+
return self.content_boxes[0].get_payload()
|
|
53
|
+
|
|
54
|
+
def get_manifest_label(self) -> str:
|
|
55
|
+
return self.manifest_label
|
|
56
|
+
|
|
57
|
+
def set_assertion_store(self, assertion_store) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Called from Manifest when assertions are changed (including when length is updated in c2pa.hash.data).
|
|
60
|
+
Reassemble the CBOR payload of the stamp with the correct hashed URIs.
|
|
61
|
+
"""
|
|
62
|
+
self.assertion_store = assertion_store
|
|
63
|
+
self._rebuild_payload()
|
|
64
|
+
|
|
65
|
+
def set_format(self, dc_format: str | None) -> None:
|
|
66
|
+
self.dc_format = dc_format
|
|
67
|
+
self._rebuild_payload()
|
|
68
|
+
|
|
69
|
+
def _build_assertions_array(self) -> list[dict[str, Any]]:
|
|
70
|
+
"""
|
|
71
|
+
Build the claim[‘assertions’] array from the current AssertionStore:
|
|
72
|
+
- url: self#jumbf=/c2pa/<manifest_label>/c2pa.assertions/<label>
|
|
73
|
+
- alg: sha256
|
|
74
|
+
- hash: sha256( JUMBF-superbox-content = description + content_boxes ) for this assertion
|
|
75
|
+
"""
|
|
76
|
+
out: list[dict[str, Any]] = []
|
|
77
|
+
if not self.assertion_store:
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
assertions = getattr(self.assertion_store, "assertions", None)
|
|
81
|
+
if assertions is None and hasattr(self.assertion_store, "get_assertions"):
|
|
82
|
+
assertions = self.assertion_store.get_assertions()
|
|
83
|
+
if not assertions:
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
for assertion in assertions:
|
|
87
|
+
label = getattr(assertion, "label", None)
|
|
88
|
+
if label is None and hasattr(assertion, "get_label"):
|
|
89
|
+
label = assertion.get_label()
|
|
90
|
+
|
|
91
|
+
if hasattr(assertion, "get_data_for_signing"):
|
|
92
|
+
data = assertion.get_data_for_signing()
|
|
93
|
+
else:
|
|
94
|
+
data = assertion.description_box.serialize() + assertion.serialize_content_boxes()
|
|
95
|
+
|
|
96
|
+
out.append(
|
|
97
|
+
{
|
|
98
|
+
"url": f"self#jumbf=/c2pa/{self.manifest_label}/c2pa.assertions/{label}",
|
|
99
|
+
"alg": "sha256",
|
|
100
|
+
"hash": _sha256(data),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
def _build_cbor_payload(self) -> bytes:
|
|
106
|
+
"""
|
|
107
|
+
Canonical CBOR content claim.
|
|
108
|
+
Include:
|
|
109
|
+
- claim_generator
|
|
110
|
+
- instanceID (stable for the object)
|
|
111
|
+
- signature (reference to c2pa.signature)
|
|
112
|
+
- optional dc:format
|
|
113
|
+
- optional assertions (if there is an assertion_store)
|
|
114
|
+
"""
|
|
115
|
+
claim: dict[str, Any] = {
|
|
116
|
+
"claim_generator": self.claim_generator,
|
|
117
|
+
"instanceID": self._instance_id,
|
|
118
|
+
"signature": self.claim_signature_label,
|
|
119
|
+
"alg": "sha256",
|
|
120
|
+
}
|
|
121
|
+
if self.dc_format:
|
|
122
|
+
claim["dc:format"] = self.dc_format
|
|
123
|
+
|
|
124
|
+
assertions_arr = self._build_assertions_array()
|
|
125
|
+
if assertions_arr:
|
|
126
|
+
claim["assertions"] = assertions_arr
|
|
127
|
+
|
|
128
|
+
return cbor2.dumps(claim, canonical=True)
|
|
129
|
+
|
|
130
|
+
def _rebuild_payload(self) -> None:
|
|
131
|
+
new_payload = self._build_cbor_payload()
|
|
132
|
+
self.content_boxes[0] = ContentBox(
|
|
133
|
+
box_type=b"cbor".hex(),
|
|
134
|
+
payload=new_payload,
|
|
135
|
+
)
|
|
136
|
+
self.sync_payload()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import cbor2
|
|
7
|
+
from cryptography import x509
|
|
8
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import Encoding
|
|
11
|
+
|
|
12
|
+
from c2pie.c2pa.claim import Claim
|
|
13
|
+
from c2pie.jumbf_boxes.content_box import ContentBox
|
|
14
|
+
from c2pie.jumbf_boxes.super_box import SuperBox
|
|
15
|
+
from c2pie.utils.content_types import c2pa_content_types
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _split_pem_certs_to_der(pem_bytes: bytes) -> list[bytes]:
|
|
19
|
+
if not pem_bytes:
|
|
20
|
+
return []
|
|
21
|
+
blocks = re.findall(
|
|
22
|
+
b"-----BEGIN CERTIFICATE-----\\s.*?-----END CERTIFICATE-----\\s*",
|
|
23
|
+
pem_bytes,
|
|
24
|
+
flags=re.DOTALL,
|
|
25
|
+
)
|
|
26
|
+
ders: list[bytes] = []
|
|
27
|
+
for blk in blocks:
|
|
28
|
+
cert = x509.load_pem_x509_certificate(blk)
|
|
29
|
+
ders.append(cert.public_bytes(Encoding.DER))
|
|
30
|
+
return ders
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClaimSignature(SuperBox):
|
|
34
|
+
"""
|
|
35
|
+
COSE_Sign1 (PS256), detached:
|
|
36
|
+
- protected: {1:-37, 33:[x5chain DER...]}
|
|
37
|
+
- unprotected: {}
|
|
38
|
+
- COSE payload = nil
|
|
39
|
+
- Sig_structure payload = bstr(Claim CBOR)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
claim: Claim,
|
|
45
|
+
*,
|
|
46
|
+
private_key: bytes,
|
|
47
|
+
certificate_pem_bundle: bytes = None,
|
|
48
|
+
certificate: bytes = None,
|
|
49
|
+
):
|
|
50
|
+
if certificate_pem_bundle is None and certificate is not None:
|
|
51
|
+
certificate_pem_bundle = certificate
|
|
52
|
+
|
|
53
|
+
self.claim = claim
|
|
54
|
+
self.private_key = private_key
|
|
55
|
+
self.certificate = certificate_pem_bundle
|
|
56
|
+
|
|
57
|
+
content_boxes = self._generate_payload()
|
|
58
|
+
super().__init__(
|
|
59
|
+
content_type=c2pa_content_types["claim_signature"],
|
|
60
|
+
label="c2pa.signature",
|
|
61
|
+
content_boxes=content_boxes,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _generate_payload(self) -> list[ContentBox]:
|
|
65
|
+
if not (self.claim and self.private_key and self.certificate):
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
cose_tagged = self._create_cose_sign1()
|
|
69
|
+
return [ContentBox(box_type=b"cbor".hex(), payload=cose_tagged)]
|
|
70
|
+
|
|
71
|
+
def set_claim(self, claim: Claim):
|
|
72
|
+
self.claim = claim
|
|
73
|
+
content_boxes = self._generate_payload()
|
|
74
|
+
super().__init__(
|
|
75
|
+
content_type=c2pa_content_types["claim_signature"],
|
|
76
|
+
label="c2pa.signature",
|
|
77
|
+
content_boxes=content_boxes,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _generate_protected_header(self) -> bytes:
|
|
81
|
+
der_chain = _split_pem_certs_to_der(self.certificate or b"")
|
|
82
|
+
protected: dict[int, Any] = {1: -37}
|
|
83
|
+
if der_chain:
|
|
84
|
+
protected[33] = der_chain
|
|
85
|
+
return cbor2.dumps(protected, canonical=True)
|
|
86
|
+
|
|
87
|
+
def _create_cose_sign1(self) -> bytes:
|
|
88
|
+
phdr_serialized = self._generate_protected_header()
|
|
89
|
+
claim_cbor = self.claim.get_cbor_payload()
|
|
90
|
+
sig_structure = ["Signature1", phdr_serialized, b"", claim_cbor]
|
|
91
|
+
to_sign = cbor2.dumps(sig_structure, canonical=True)
|
|
92
|
+
|
|
93
|
+
key = serialization.load_pem_private_key(self.private_key, password=None)
|
|
94
|
+
signature = key.sign( # type: ignore
|
|
95
|
+
to_sign,
|
|
96
|
+
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32), # type: ignore
|
|
97
|
+
hashes.SHA256(), # type: ignore
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
cose_msg = [phdr_serialized, {}, None, signature]
|
|
101
|
+
return cbor2.dumps(cbor2.CBORTag(18, cose_msg), canonical=True)
|
c2pie/c2pa/config.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RETRY_SIGNATURE = 8 # max retries, might be terminated earliers
|
c2pie/c2pa/manifest.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from c2pie.jumbf_boxes.super_box import SuperBox
|
|
4
|
+
from c2pie.utils.content_types import c2pa_content_types
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Manifest(SuperBox):
|
|
8
|
+
"""
|
|
9
|
+
C2PA Manifest: Assertion Store + Claim + Claim Signature.
|
|
10
|
+
The manifest label is compatible with c2patool: urn:uuid:<uuid-hex>
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
manifest_label: str = f"urn:uuid:{uuid.uuid4().hex}",
|
|
16
|
+
):
|
|
17
|
+
self.manifest_label = manifest_label
|
|
18
|
+
self.claim = None
|
|
19
|
+
self.claim_signature = None
|
|
20
|
+
self.assertion_store = None
|
|
21
|
+
|
|
22
|
+
super().__init__(content_type=c2pa_content_types["default_manifest"], label=self.manifest_label)
|
|
23
|
+
|
|
24
|
+
def set_claim(self, claim):
|
|
25
|
+
self.claim = claim
|
|
26
|
+
self.add_content_box(self.claim)
|
|
27
|
+
|
|
28
|
+
def set_claim_signature(self, claim_signature):
|
|
29
|
+
self.claim_signature = claim_signature
|
|
30
|
+
self.add_content_box(self.claim_signature)
|
|
31
|
+
|
|
32
|
+
def set_assertion_store(self, assertion_store):
|
|
33
|
+
self.assertion_store = assertion_store
|
|
34
|
+
self.add_content_box(self.assertion_store)
|
|
35
|
+
|
|
36
|
+
def get_manifest_label(self):
|
|
37
|
+
return self.manifest_label
|
|
38
|
+
|
|
39
|
+
def get_assertions(self):
|
|
40
|
+
if self.assertion_store:
|
|
41
|
+
return self.assertion_store.get_assertions()
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def set_hash_data_length(self, length: int):
|
|
45
|
+
"""
|
|
46
|
+
Updates the length of exceptions in HashData, reassembles Claim (assertion hashes)
|
|
47
|
+
and ClaimSignature (COSE Sign1 detached over Claim CBOR).
|
|
48
|
+
"""
|
|
49
|
+
if self.assertion_store and self.claim and self.claim_signature:
|
|
50
|
+
self.assertion_store.set_hash_data_length(length)
|
|
51
|
+
|
|
52
|
+
self.claim.set_assertion_store(self.assertion_store)
|
|
53
|
+
|
|
54
|
+
self.claim_signature.set_claim(self.claim)
|
|
55
|
+
|
|
56
|
+
self.sync_payload()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from c2pie.jumbf_boxes.super_box import SuperBox
|
|
4
|
+
from c2pie.utils.content_types import c2pa_content_types
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ManifestStore(SuperBox):
|
|
8
|
+
"""
|
|
9
|
+
C2PA Manifest Store (JUMBF superbox) with one or more Manifest.
|
|
10
|
+
IMPORTANT: here we do NOT "assume" length of specific manifest bytes (JPG/PDF, etc.).
|
|
11
|
+
For PDF, the length of the exception is set by the injector; for JPG, by its own injector.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, manifests: list | None = None):
|
|
15
|
+
self.manifests: list = [] if manifests is None else manifests
|
|
16
|
+
super().__init__(
|
|
17
|
+
content_type=c2pa_content_types["manifest_store"],
|
|
18
|
+
label="c2pa",
|
|
19
|
+
content_boxes=self.manifests,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def sync_payload(self):
|
|
23
|
+
super().sync_payload()
|
|
24
|
+
|
|
25
|
+
def set_hash_data_length_for_all(
|
|
26
|
+
self,
|
|
27
|
+
length: int,
|
|
28
|
+
) -> None:
|
|
29
|
+
for manifest in self.manifests:
|
|
30
|
+
manifest.set_hash_data_length(length)
|
|
31
|
+
super().sync_payload()
|
|
32
|
+
|
|
33
|
+
def serialize(self) -> bytes:
|
|
34
|
+
return super().serialize()
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
JPG_SEGMENT_MAX_PAYLOAD_LENGTH = 65517
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class JpgSegment:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
payload_length: int,
|
|
8
|
+
marker: bytes = bytes.fromhex("EB"),
|
|
9
|
+
): # noqa: B008
|
|
10
|
+
self.marker = marker
|
|
11
|
+
self.payload_length = payload_length
|
|
12
|
+
|
|
13
|
+
def get_segment_length(self):
|
|
14
|
+
return self.payload_length + 2 # payload length + size of payload length
|
|
15
|
+
|
|
16
|
+
def serialize(
|
|
17
|
+
self,
|
|
18
|
+
payload: bytes,
|
|
19
|
+
):
|
|
20
|
+
serialized_data = b""
|
|
21
|
+
|
|
22
|
+
serialized_data += bytes.fromhex("FF") + self.marker
|
|
23
|
+
serialized_data += self.get_segment_length().to_bytes(2, "big")
|
|
24
|
+
serialized_data += payload
|
|
25
|
+
|
|
26
|
+
return serialized_data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JpgSegmentApp11(JpgSegment):
|
|
30
|
+
def __init__(self, segment_id, sequence_number, payload_length, payload):
|
|
31
|
+
self.ci = bytes.fromhex(b"JP".hex()) # 2 bytes
|
|
32
|
+
self.en = segment_id # 2 bytes
|
|
33
|
+
self.z = sequence_number # 4 bytes
|
|
34
|
+
|
|
35
|
+
self.app11_payload = payload
|
|
36
|
+
|
|
37
|
+
super().__init__(payload_length=self.get_payload_length(payload_length))
|
|
38
|
+
|
|
39
|
+
def get_payload_length(self, payload_length):
|
|
40
|
+
return 2 + 2 + 4 + 4 + 4 + payload_length
|
|
41
|
+
|
|
42
|
+
def serialize(self, payload=None):
|
|
43
|
+
_en = self.en.to_bytes(2, "big")
|
|
44
|
+
_z = self.z.to_bytes(4, "big")
|
|
45
|
+
|
|
46
|
+
app11_payload = self.ci + _en + _z + self.app11_payload
|
|
47
|
+
|
|
48
|
+
return super().serialize(app11_payload)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class JpgSegmentApp11Storage:
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
app11_segment_box_length: int,
|
|
55
|
+
app11_segment_box_type: str,
|
|
56
|
+
payload: bytes,
|
|
57
|
+
):
|
|
58
|
+
self.l_box = app11_segment_box_length
|
|
59
|
+
self.t_box = app11_segment_box_type
|
|
60
|
+
self.payload = payload
|
|
61
|
+
self.serialized_length = 0
|
|
62
|
+
|
|
63
|
+
def get_payload_length(self):
|
|
64
|
+
return self.l_box - 4 - 4 # - t_box_length - l_box_length
|
|
65
|
+
|
|
66
|
+
def get_serialized_length(self):
|
|
67
|
+
return self.serialized_length
|
|
68
|
+
|
|
69
|
+
def serialize(self):
|
|
70
|
+
segment_id = 1
|
|
71
|
+
sequence_number = 0
|
|
72
|
+
payload_offset = 0
|
|
73
|
+
|
|
74
|
+
payload_length = self.get_payload_length()
|
|
75
|
+
|
|
76
|
+
app11_segments = []
|
|
77
|
+
|
|
78
|
+
while payload_length > 0:
|
|
79
|
+
sequence_number += 1
|
|
80
|
+
chunk_length = JPG_SEGMENT_MAX_PAYLOAD_LENGTH
|
|
81
|
+
if payload_length < JPG_SEGMENT_MAX_PAYLOAD_LENGTH:
|
|
82
|
+
chunk_length = payload_length
|
|
83
|
+
|
|
84
|
+
app11_segments.append(
|
|
85
|
+
JpgSegmentApp11(
|
|
86
|
+
segment_id=segment_id,
|
|
87
|
+
sequence_number=sequence_number,
|
|
88
|
+
payload_length=chunk_length,
|
|
89
|
+
payload=self.payload[payload_offset:],
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
payload_length -= chunk_length
|
|
93
|
+
payload_offset += chunk_length
|
|
94
|
+
|
|
95
|
+
serialized_storage_data = b""
|
|
96
|
+
for app11_segment in app11_segments:
|
|
97
|
+
serialized_storage_data += app11_segment.serialize()
|
|
98
|
+
|
|
99
|
+
self.serialized_length = len(serialized_storage_data)
|
|
100
|
+
return serialized_storage_data
|