diff --git a/fiftyone_pipeline_did/examples/fodid_example.py b/fiftyone_pipeline_did/examples/fodid_example.py index c9e7e17..cdd2446 100644 --- a/fiftyone_pipeline_did/examples/fodid_example.py +++ b/fiftyone_pipeline_did/examples/fodid_example.py @@ -45,8 +45,8 @@ def sample_payload(): payload[FodId.FLAGS_OFFSET] = 0x00 payload[FodId.LICENSE_ID_OFFSET:FodId.LICENSE_ID_OFFSET + 4] = \ bytes([0x78, 0x56, 0x34, 0x12]) - for i in range(FodId.HASH_LENGTH): - payload[FodId.HASH_OFFSET + i] = 0x20 + i + for i in range(FodId.MATCH_KEY_LENGTH): + payload[FodId.MATCH_KEY_OFFSET + i] = 0x20 + i return bytes(payload) diff --git a/fiftyone_pipeline_did/readme.md b/fiftyone_pipeline_did/readme.md index b314d0a..4e8aedd 100644 --- a/fiftyone_pipeline_did/readme.md +++ b/fiftyone_pipeline_did/readme.md @@ -104,6 +104,13 @@ nothing outside 51Degrees. alias returns the same bytes and warns with `DeprecationWarning`, and the alias will be removed in a future release, so move callers to `match_key`. +The class constants naming the match key field follow the same +vocabulary, being `FodId.MATCH_KEY_OFFSET` and `FodId.MATCH_KEY_LENGTH`. +`FodId.HASH_OFFSET` and `FodId.HASH_LENGTH` remain as deprecated aliases +holding the same values, and a class constant cannot warn when it is +read, so move callers to the new names before the aliases are removed in +a future release. + ## Parsing without exceptions An identifier arriving from outside, in a query string, a header or a diff --git a/fiftyone_pipeline_did/src/fiftyone_pipeline_did/did_client.py b/fiftyone_pipeline_did/src/fiftyone_pipeline_did/did_client.py index 39f5b8d..b84feff 100644 --- a/fiftyone_pipeline_did/src/fiftyone_pipeline_did/did_client.py +++ b/fiftyone_pipeline_did/src/fiftyone_pipeline_did/did_client.py @@ -764,7 +764,7 @@ def _payload_length_valid(fod_id: FodId) -> bool: whose exact lengths belong to the cloud, so any longer payload is accepted here.""" match_key_length = FodId.GUID_LENGTH if fod_id.type is IdType.RANDOM \ - else FodId.HASH_LENGTH + else FodId.MATCH_KEY_LENGTH return len(fod_id.payload) >= FodId.HEADER_LENGTH + match_key_length diff --git a/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py b/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py index 969b41b..2d6578f 100644 --- a/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py +++ b/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py @@ -181,17 +181,29 @@ class FodId: #: Byte length of the License Id field. LICENSE_ID_LENGTH = 4 #: Byte offset of the match key field within the payload. - HASH_OFFSET = 5 + MATCH_KEY_OFFSET = 5 #: Byte length of the match key field (SHA-256). - HASH_LENGTH = 32 + MATCH_KEY_LENGTH = 32 #: Byte length of the header (Flags + License Id) common to every type. - HEADER_LENGTH = HASH_OFFSET + HEADER_LENGTH = MATCH_KEY_OFFSET #: Byte length of the GUID match key carried by Random identifiers. GUID_LENGTH = 16 #: Minimum byte length of a Random 51Did payload. RANDOM_PAYLOAD_LENGTH = HEADER_LENGTH + GUID_LENGTH #: Minimum byte length of a Probabilistic or HashedEmail 51Did payload. - PAYLOAD_LENGTH = HASH_OFFSET + HASH_LENGTH + PAYLOAD_LENGTH = MATCH_KEY_OFFSET + MATCH_KEY_LENGTH + #: Deprecated alias for :attr:`MATCH_KEY_OFFSET`. The stable, + #: comparable part of a 51Did is now called the match key, mirroring + #: the Model Terms for Marketing vocabulary. A class constant cannot + #: warn when it is read, so this alias holds the same value and will + #: be removed in a future release. + HASH_OFFSET = MATCH_KEY_OFFSET + #: Deprecated alias for :attr:`MATCH_KEY_LENGTH`. The stable, + #: comparable part of a 51Did is now called the match key, mirroring + #: the Model Terms for Marketing vocabulary. A class constant cannot + #: warn when it is read, so this alias holds the same value and will + #: be removed in a future release. + HASH_LENGTH = MATCH_KEY_LENGTH def __init__(self, owid: Owid) -> None: """Promotes an already-parsed :class:`~fiftyone_pipeline_did.Owid` @@ -531,7 +543,8 @@ def _read_payload(payload: bytes) -> Tuple[FodIdParseStatus, int, int, bytes]: # bytes is immutable, so slicing yields a match key that cannot be used # to change the underlying payload and no defensive copy is required. match_key = bytes( - payload[FodId.HASH_OFFSET:FodId.HASH_OFFSET + match_key_length]) + payload[FodId.MATCH_KEY_OFFSET: + FodId.MATCH_KEY_OFFSET + match_key_length]) return FodIdParseStatus.PARSED, flags, license_id, match_key @@ -550,7 +563,7 @@ def _match_key_length(id_type: IdType, payload: bytes) -> int: return FodId.GUID_LENGTH if id_type is IdType.RESERVED: return len(payload) - FodId.HEADER_LENGTH - return FodId.HASH_LENGTH + return FodId.MATCH_KEY_LENGTH def _payload_message(status: FodIdParseStatus, payload: bytes) -> str: diff --git a/fiftyone_pipeline_did/tests/envelope.py b/fiftyone_pipeline_did/tests/envelope.py index 2c2bf56..e68177d 100644 --- a/fiftyone_pipeline_did/tests/envelope.py +++ b/fiftyone_pipeline_did/tests/envelope.py @@ -47,8 +47,8 @@ def probabilistic_payload(): payload[FodId.FLAGS_OFFSET] = 0b0000_0101 payload[FodId.LICENSE_ID_OFFSET:FodId.LICENSE_ID_OFFSET + 4] = \ bytes([0x78, 0x56, 0x34, 0x12]) - for i in range(FodId.HASH_LENGTH): - payload[FodId.HASH_OFFSET + i] = 0x20 + i + for i in range(FodId.MATCH_KEY_LENGTH): + payload[FodId.MATCH_KEY_OFFSET + i] = 0x20 + i return bytes(payload) @@ -57,7 +57,7 @@ def random_payload(): payload = bytearray(FodId.RANDOM_PAYLOAD_LENGTH) payload[FodId.FLAGS_OFFSET] = (1 << 6) | 0b001 for i in range(FodId.GUID_LENGTH): - payload[FodId.HASH_OFFSET + i] = 0x40 + i + payload[FodId.MATCH_KEY_OFFSET + i] = 0x40 + i return bytes(payload) diff --git a/fiftyone_pipeline_did/tests/test_did_client.py b/fiftyone_pipeline_did/tests/test_did_client.py index 2508ffc..25a9eaa 100644 --- a/fiftyone_pipeline_did/tests/test_did_client.py +++ b/fiftyone_pipeline_did/tests/test_did_client.py @@ -77,8 +77,8 @@ def setUp(self): # encoded: every three bytes of 0xFB encode to "+/v7" whatever # the alignment, and a 32 byte run holds several whole triples. payload = bytearray(probabilistic_payload()) - for i in range(FodId.HASH_LENGTH): - payload[FodId.HASH_OFFSET + i] = 0xFB + for i in range(FodId.MATCH_KEY_LENGTH): + payload[FodId.MATCH_KEY_OFFSET + i] = 0xFB self.fod_id = signed_fod_id(self.crypto, bytes(payload)) self.standard = self.fod_id.as_base64() self.assertTrue("+" in self.standard or "/" in self.standard, @@ -491,8 +491,8 @@ def test_string_form_is_sent_as_given_and_encoded(self): # A payload of 0xFB bytes encodes to "+/v7" whatever the alignment, # so the standard form carries both characters that need encoding. payload = bytearray(probabilistic_payload()) - for i in range(FodId.HASH_LENGTH): - payload[FodId.HASH_OFFSET + i] = 0xFB + for i in range(FodId.MATCH_KEY_LENGTH): + payload[FodId.MATCH_KEY_OFFSET + i] = 0xFB standard = signed_fod_id(Crypto.new(), bytes(payload)).as_base64() self.assertIn("+", standard) self.assertIn("/", standard) diff --git a/fiftyone_pipeline_did/tests/test_fodid.py b/fiftyone_pipeline_did/tests/test_fodid.py index e50df93..7f3b4b8 100644 --- a/fiftyone_pipeline_did/tests/test_fodid.py +++ b/fiftyone_pipeline_did/tests/test_fodid.py @@ -42,7 +42,7 @@ # 0xA5: usage bits plus the HashedEmail type tag in bits 6-7. CANONICAL_FLAGS = 0xA5 CANONICAL_LICENSE_ID = 0x12345678 -CANONICAL_MATCH_KEY = bytes((0x20 + i) for i in range(FodId.HASH_LENGTH)) +CANONICAL_MATCH_KEY = bytes((0x20 + i) for i in range(FodId.MATCH_KEY_LENGTH)) #: A creator domain longer than the one the cloud signs with, as a #: self-hosted container may be configured to use. @@ -61,7 +61,8 @@ def canonical_payload(): payload = bytearray(FodId.PAYLOAD_LENGTH) payload[FodId.FLAGS_OFFSET] = CANONICAL_FLAGS _write_license_id(payload) - payload[FodId.HASH_OFFSET:FodId.HASH_OFFSET + FodId.HASH_LENGTH] = \ + payload[FodId.MATCH_KEY_OFFSET: + FodId.MATCH_KEY_OFFSET + FodId.MATCH_KEY_LENGTH] = \ CANONICAL_MATCH_KEY return bytearray(payload) @@ -71,7 +72,7 @@ def canonical_random_payload(): payload[FodId.FLAGS_OFFSET] = (1 << 6) | 0b001 # Random tag + usage bits _write_license_id(payload) for i in range(FodId.GUID_LENGTH): - payload[FodId.HASH_OFFSET + i] = 0x40 + i + payload[FodId.MATCH_KEY_OFFSET + i] = 0x40 + i return bytearray(payload) @@ -104,13 +105,20 @@ def setUp(self): # ----- Current .NET coverage ----- def test_constants_are_internally_consistent(self): - self.assertEqual(FodId.HASH_OFFSET + FodId.HASH_LENGTH, + self.assertEqual(FodId.MATCH_KEY_OFFSET + FodId.MATCH_KEY_LENGTH, FodId.PAYLOAD_LENGTH) self.assertEqual(FodId.LICENSE_ID_OFFSET + FodId.LICENSE_ID_LENGTH, - FodId.HASH_OFFSET) - self.assertEqual(FodId.HASH_OFFSET + FodId.GUID_LENGTH, + FodId.MATCH_KEY_OFFSET) + self.assertEqual(FodId.MATCH_KEY_OFFSET + FodId.GUID_LENGTH, FodId.RANDOM_PAYLOAD_LENGTH) + def test_deprecated_constant_aliases_hold_the_new_values(self): + # The offset and length of the match key field are now named after + # the match key. The old names stay for a release as aliases + # holding the same values. + self.assertEqual(FodId.MATCH_KEY_OFFSET, FodId.HASH_OFFSET) + self.assertEqual(FodId.MATCH_KEY_LENGTH, FodId.HASH_LENGTH) + def test_exposes_owid_level_fields(self): fod = FodId.from_base64( self.factory.signed_owid_base64(canonical_payload())) @@ -235,7 +243,7 @@ def test_payload_larger_than_spec_uses_first_37_bytes(self): self.assertEqual(CANONICAL_FLAGS, fod.flags) self.assertEqual(CANONICAL_LICENSE_ID, fod.license_id) self.assertEqual(CANONICAL_MATCH_KEY, fod.match_key) - self.assertEqual(FodId.HASH_LENGTH, len(fod.match_key)) + self.assertEqual(FodId.MATCH_KEY_LENGTH, len(fod.match_key)) def test_long_envelope_parses_and_keeps_the_header_fields(self): # No upper bound belongs in the reader: a creator domain is a @@ -250,7 +258,7 @@ def test_long_envelope_parses_and_keeps_the_header_fields(self): self.assertEqual(CANONICAL_FLAGS, fod.flags) self.assertEqual(CANONICAL_LICENSE_ID, fod.license_id) self.assertEqual(CANONICAL_MATCH_KEY, fod.match_key) - self.assertEqual(FodId.HASH_LENGTH, len(fod.match_key)) + self.assertEqual(FodId.MATCH_KEY_LENGTH, len(fod.match_key)) def test_is_cryptographically_verifiable(self): fod = FodId.from_base64( @@ -316,7 +324,7 @@ def test_hashed_email_payload_one_byte_short_raises(self): FodId.from_base64(base64) def test_reserved_header_only_parses(self): - payload = bytearray(FodId.HASH_OFFSET) + payload = bytearray(FodId.MATCH_KEY_OFFSET) payload[FodId.FLAGS_OFFSET] = 0b1100_0000 fod = FodId.from_base64(self.factory.signed_owid_base64(payload)) self.assertEqual(IdType.RESERVED, fod.type) @@ -367,7 +375,7 @@ def test_source_envelope_cannot_be_changed_after_construction(self): with self.assertRaises(AttributeError): owid.signature = bytes(64) self.assertEqual(CANONICAL_MATCH_KEY, fod.match_key) - self.assertEqual(0x20, fod.payload[FodId.HASH_OFFSET]) + self.assertEqual(0x20, fod.payload[FodId.MATCH_KEY_OFFSET]) def test_constructor_reads_the_envelope_back_through_the_parser(self): # The envelope handed in is written out and read back, so the FodId @@ -487,7 +495,7 @@ def test_longer_creator_context_section_is_accepted(self): fod = self.assert_parsed(FodId.try_from_base64( self.factory.signed_owid_base64(payload))) self.assert_canonical(fod) - self.assertEqual(FodId.HASH_LENGTH, len(fod.match_key)) + self.assertEqual(FodId.MATCH_KEY_LENGTH, len(fod.match_key)) self.assertEqual(payload, fod.payload) def test_far_longer_payload_is_not_rejected_for_its_length(self):