Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ public class Main {

private static final String DOMAIN = "51degrees.com";

// This example stands in for the cloud, so it builds a payload byte by
// byte, which is a writer's job and not a reader's. The layout is not
// part of the reader's public surface, so the offsets are spelled out
// here from the specification at
// https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
// Code that reads a 51Did should use the typed accessors instead.
private static final int LICENSE_ID_OFFSET = 1;
private static final int MATCH_KEY_OFFSET = 5;
private static final int MATCH_KEY_LENGTH = 32;
private static final int PAYLOAD_LENGTH =
MATCH_KEY_OFFSET + MATCH_KEY_LENGTH;

public static class Example {

public void run() throws Exception {
Expand All @@ -57,13 +69,15 @@ public void run() throws Exception {
FodId fodId = FodId.fromBase64(issue(creator, payload));

System.out.println("51Did parsed from base64:");
System.out.println(" Domain : " + fodId.getDomain());
System.out.println(" Type : " + fodId.getType());
System.out.println(" Flags : 0x"
+ Integer.toHexString(fodId.getFlags()));
System.out.println(" LicenseId : " + fodId.getLicenseId());
System.out.println(" Match key : " + toHex(fodId.getMatchKey()));
System.out.println(" Verifies : "
System.out.println(" Domain : " + fodId.getDomain());
System.out.println(" Type : " + fodId.getType());
System.out.println(" Usage : " + fodId.getUsage());
System.out.println(" From consent : "
+ fodId.isUsageFromConsent());
System.out.println(" LicenseId : " + fodId.getLicenseId());
System.out.println(" Match key : "
+ toHex(fodId.getMatchKey()));
System.out.println(" Verifies : "
+ fodId.verify(crypto.publicKeyPem()));

// Issue the SAME payload again: a separate envelope, same match
Expand Down Expand Up @@ -99,18 +113,20 @@ private String issue(Creator creator, byte[] payload)
}

/**
* A canonical 37-byte Probabilistic payload: flags 0x00, License Id
* A canonical 37-byte Probabilistic payload with flags 0x03, License Id
* 0x12345678 (little-endian) and a 32-byte match key 0x20..0x3F.
*/
private byte[] samplePayload() {
byte[] payload = new byte[FodId.PAYLOAD_LENGTH];
payload[FodId.FLAGS_OFFSET] = 0x00;
payload[FodId.LICENSE_ID_OFFSET] = 0x78;
payload[FodId.LICENSE_ID_OFFSET + 1] = 0x56;
payload[FodId.LICENSE_ID_OFFSET + 2] = 0x34;
payload[FodId.LICENSE_ID_OFFSET + 3] = 0x12;
for (int i = 0; i < FodId.MATCH_KEY_LENGTH; i++) {
payload[FodId.MATCH_KEY_OFFSET + i] = (byte) (0x20 + i);
byte[] payload = new byte[PAYLOAD_LENGTH];
// Flags 0b0000_0011, being standard marketing usage stated by
// the caller, on a Probabilistic identifier (bits 6-7 zero).
payload[0] = 0b0000_0011;
payload[LICENSE_ID_OFFSET] = 0x78;
payload[LICENSE_ID_OFFSET + 1] = 0x56;
payload[LICENSE_ID_OFFSET + 2] = 0x34;
payload[LICENSE_ID_OFFSET + 3] = 0x12;
for (int i = 0; i < MATCH_KEY_LENGTH; i++) {
payload[MATCH_KEY_OFFSET + i] = (byte) (0x20 + i);
}
return payload;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,24 @@ private static String keyList(Crypto crypto) {
}

/**
* A canonical 37-byte Probabilistic payload: flags 0x00, License Id
* 0x12345678 (little-endian) and a 32-byte match key 0x20..0x3F.
* A canonical 37-byte Probabilistic payload with flags 0x00, License Id
* 0x12345678 (little-endian) and a 32-byte match key 0x20..0x3F. The
* offsets are spelled out here because writing a payload is the cloud's
* job rather than a reader's, so the layout is not part of the reader's
* public surface. It is specified at
* https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
*/
private static byte[] samplePayload() {
byte[] payload = new byte[FodId.PAYLOAD_LENGTH];
payload[FodId.LICENSE_ID_OFFSET] = 0x78;
payload[FodId.LICENSE_ID_OFFSET + 1] = 0x56;
payload[FodId.LICENSE_ID_OFFSET + 2] = 0x34;
payload[FodId.LICENSE_ID_OFFSET + 3] = 0x12;
for (int i = 0; i < FodId.MATCH_KEY_LENGTH; i++) {
payload[FodId.MATCH_KEY_OFFSET + i] = (byte) (0x20 + i);
final int licenseIdOffset = 1;
final int matchKeyOffset = 5;
final int matchKeyLength = 32;
byte[] payload = new byte[matchKeyOffset + matchKeyLength];
payload[licenseIdOffset] = 0x78;
payload[licenseIdOffset + 1] = 0x56;
payload[licenseIdOffset + 2] = 0x34;
payload[licenseIdOffset + 3] = 0x12;
for (int i = 0; i < matchKeyLength; i++) {
payload[matchKeyOffset + i] = (byte) (0x20 + i);
}
return payload;
}
Expand Down
83 changes: 61 additions & 22 deletions pipeline.did/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ envelopes.**

## Payload layout

The header is shared by every identifier type. Bits 6-7 of Flags select the
type and the length of the match key that follows.

| Offset | Length | Field | Type |
|-------:|-------:|------------|-------------------------------------------------|
| 0 | 1 | Flags | uint8: bits 0-2 usage, bits 6-7 identifier type |
| 1 | 4 | LicenseId | uint32 (little-endian) |
| 5 | 16/32 | Match key | SHA-256 (Probabilistic, HashedEmail) or GUID (Random) |
| after | any | Context | Optional creator context section, readable only by 51Degrees |
The byte layout of a 51Did is specified in
[identifier-layout.md](https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md),
and the surface every 51Did package offers is specified in
[package-surface.md](https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md).
Those two pages are the authority, so read them rather than working the
layout out from this package. What follows is a summary of the part that
changes how this package behaves.

The identifier carries a five byte header of Flags and License Id, then the
match key, then an optional creator context section. Bits 6-7 of Flags
select the type, which decides how long the match key is and so what the
least a payload can hold is.

| Bits 7-6 | `IdType` | Match key length | Minimum payload |
|---------:|-----------------|-------------:|----------------:|
Expand All @@ -52,6 +55,12 @@ License Id bytes hold an encrypted value that only 51Degrees can turn back
into a licence identifier, so `getLicenseId()` is the field's raw value and
identifies nothing outside 51Degrees.

The package gives no way to read the payload by hand. There is no raw flags
accessor and the offsets and lengths are package-private, because every
field and every bit already has a typed accessor and reading the bytes by
hand is how the usage gets misread. See the usage section below for the
mistake this closes off.

## OWID dependency

`FodId` builds on the OWID envelope library
Expand Down Expand Up @@ -167,17 +176,20 @@ apart from "the signature could not be checked" (`KEY_UNAVAILABLE`,
```java
import fiftyone.pipeline.did.FodId;
import fiftyone.pipeline.did.IdType;
import fiftyone.pipeline.did.Usage;
import java.time.Instant;

FodId fodId = FodId.fromBase64(base64FromCloudService);

int flags = fodId.getFlags();
IdType type = fodId.getType(); // PROBABILISTIC / RANDOM / HASHED_EMAIL
long licenseId = fodId.getLicenseId();
byte[] matchKey = fodId.getMatchKey(); // SHA-256 or GUID bytes, see type
IdType type = fodId.getType(); // PROBABILISTIC / RANDOM / HASHED_EMAIL
Usage usage = fodId.getUsage(); // what the identifier may be used for
boolean fromConsent = fodId.isUsageFromConsent();
long licenseId = fodId.getLicenseId();
byte[] matchKey = fodId.getMatchKey(); // SHA-256 or GUID bytes, see type

// Delegated OWID-level fields and operations.
String domain = fodId.getDomain();
long minutes = fodId.getDateMinutes(); // the envelope's own date field
Instant date = fodId.getDate(); // when the cloud issued it
boolean verified = fodId.verify(publicKeyPem);
String base64 = fodId.asBase64(); // standard alphabet, padded
String forUrl = fodId.asBase64Url(); // URL-safe alphabet, no padding
Expand All @@ -194,15 +206,42 @@ FodId b = FodId.fromBase64(idprobglobalB);
boolean sameMatchKey = java.util.Arrays.equals(a.getMatchKey(), b.getMatchKey());
```

Use `getMatchKey()` as the cache / dedup key. `getHash()` remains as a
deprecated alias of `getMatchKey()`, returning the same bytes, and will be
removed in a future release.
Use `getMatchKey()` as the cache / dedup key.

## What a 51Did may be used for

The payload constants follow the same naming. `MATCH_KEY_OFFSET` and
`MATCH_KEY_LENGTH` give the position and the size of the match key inside the
payload, and `HASH_OFFSET` and `HASH_LENGTH` remain as deprecated aliases of
the same two values so that code written against the earlier names keeps
compiling. The aliases will be removed in a future release.
`getUsage()` answers what the identifier was created for, and it is the
accessor a data protection decision turns on.

| `Usage` | Cloud `id.usage` | What it means |
|---|---|---|
| `NON_MARKETING` | `non-marketing` | Created for use that is not marketing. Must never be passed to a demand source. |
| `STANDARD` | `standard` | Created for standard marketing, being targeting unrelated to the person's browsing history or interactions. |
| `PERSONALIZED` | `personalized` | Created for personalized marketing, being targeting related to the person's browsing history or interactions. |
| `NONE` | none | No usage bit is set. The cloud never issues such an identifier, so treat it as one that may not be passed on. |

`STANDARD` and `PERSONALIZED` may be passed only to a recipient that has
accepted the applicable terms.

The three usages are cumulative in the byte rather than exclusive, because
non-marketing sets one bit, standard sets two and personalized sets three,
so every marketing identifier also carries the non-marketing bit. Code that
masked the byte for that bit alone would read every marketing identifier as
non-marketing, which is the wrong way round for a rule that says a
non-marketing identifier must never reach a demand source. `getUsage()`
answers with the highest usage granted, so that mistake cannot be made, and
the package offers no raw flags accessor with which to make it.

`isUsageFromConsent()` says whether the usage came from an IAB consent
string the caller sent rather than being stated by the caller directly.
Both are legitimate ways to arrive at a usage and this says nothing about
which usage it is.

```java
if (fodId.getUsage() == Usage.NON_MARKETING) {
// Do not pass this identifier to a demand source.
}
```

## Verifying on your server

Expand Down
Loading
Loading