Sample implementation for the Key ID derivation logic in different programming languages such as Python, JavaScript/NodeJS and Java.

SPEKE Key ID Override Samples

Overview

This page provides code samples for implementing the Key ID derivation logic (the hashing function) in different languages.

Implementation for SPEKE v1 and SPEKE v2 slightly differs.

Samples are provided in a few popular languages, but the algorithm can be implemented in any language.

See SPEKE Key ID Override for the full documentation on this feature.

Algorithm

Essentially, we take all the input parameters contributing to the uniqueness of the Key ID, compute a SHA256 hash of them, and then XOR the first and second halves of the hash to reduce the output to 128 bits. The resulting 128 bits are then converted to a GUID.

The input parameters for the algorithm slightly vary for SPEKE v1 and SPEKE v2.

All the parameters are string values.

Common parameters for SPEKE v1 and SPEKE v2:

  • TenantId - Customer’s Axinom DRM Tenant ID (to be found under My Mosaic / DRM / Key Service configuration).

  • Content ID - The identifier of a video asset or a stream. It’s called "Resource ID" in AWS MediaConvert and MediaPackage. In the request CPIX, it is the id attribute of the root cpix:CPIX element for SPEKE v1 and the contentId attribute for SPEKE v2.

  • Content Key Period Index - When key rotation is enabled, each SPEKE key request contains a zero-based sequential period index number. Use "0" when no key rotation is enabled/supported.

SPEKE v1 specific parameters:

  • Key ID Index - the Key ID index in the request. Should be set to "0", as the SPEKE v1 protocol specifies only one key per request.

Tip
Axinom implementation supports multiple keys even with SPEKE v1 request, therefore, a non-zero value can also be set for multiple keys.

SPEKE v2 specific parameters:

  • Protection Scheme - "cenc" or "cbcs" encryption scheme used for packaging content.

  • Intended Track Type - When multiple keys are requested, the intended-track-type shall be set.

Tip
For example, AWS sets "VIDEO" for the first content key and "AUDIO" for the second content key.

The algorithm is as follows:

For SPEKE v1: InputParameters = "<TenantId>" + "<Content ID>" + "<Content Key Period Index>" + "<Key ID Index>"

For SPEKE v2: InputParameters = "<TenantId>" + "<Content ID>" + "<Protection Scheme>" + "<Content Key Period Index>" + "<Intended Track Type>"

BaseHash = SHA256(InputParameters)

PartOneHash = Take first 16 bytes of BaseHash

PartTwoHash = Take the last 16 bytes of BaseHash

XorResult = PartOneHash XOR PartTwoHash

Key ID = XorResult to GUID // GUID must be generated using little-endian byte order as in Microsoft .NET.

General Notes

All samples expect an input string called inputParameters.

It’s value should be a concatenation of the input parameters depending on the SPEKE version:

For SPEKE v1:

inputParameters = tenantId + contentId + contentKeyPeriodIndex + keyIdIndex

For SPEKE v2:

inputParameters = tenantId + contentId + protectionScheme + contentKeyPeriodIndex + intendedTrackType

All samples calculate a variable called kid which is the final Key ID.

The expected results for the provided samples are:

For SPEKE v1:

Parameter Value

tenantId

10d42897-a795-4fd8-a2d4-00e3ab59dece

contentId

bd99b041-4353-4b7a-9533-f36ee752b735

contentKeyPeriodIndex

0

keyIdIndex

0

Expected Key ID

0a1e610d-e346-0665-42b2-409580b51be6

For SPEKE v2:

Parameter Value

tenantId

10d42897-a795-4fd8-a2d4-00e3ab59dece

contentId

test_content

protectionScheme

cenc

contentKeyPeriodIndex

0

intendedTrackType

VIDEO

Expected Key ID

bc8b57c8-6a1e-1b58-5235-d8be6ce5602a

Python 3

import hashlib
import uuid

inputParameters = "<see above>"

baseToHashAsBytes = str.encode(inputParameters)

# SHA256 hash
hash = hashlib.sha256()
hash.update(baseToHashAsBytes)
baseHashValue = hash.digest()

# XOR the first and second half of the base hash to make a 16 bytes hash
hashPartOne = baseHashValue[:16]
hashPartTwo = baseHashValue[16:32]
xorHash = bytes([_a ^ _b for _a, _b in zip(hashPartOne, hashPartTwo)])

# Convert to a GUID (based on little-endian bytes)
kid = uuid.UUID(bytes_le=xorHash)

print(kid)

JavaScript / NodeJS

No 3rd party modules are required for the JavaScript / NodeJS implementation. But of course some modules can be used to simplify the code (e.g., for producing a GUID).

const crypto = require("crypto");

const inputParameters = "<see above>";
var baseToHashAsBytes = Buffer.from(inputParameters);

// SHA256 hash
var sha256Hasher = crypto.createHash("sha256");
sha256Hasher.update(buffbaseToHashAsByteser);
const baseHashValue = sha256Hasher.digest();

// XOR the first and second half of the base hash to make a 16 bytes hash
const partOne = baseHashValue.slice(0, 16);
const partTwo = baseHashValue.slice(16, 32);

let xorHash = Buffer.alloc(16);
for (let n = 0; n < 16; n++)
    xorHash[n] = partOne[n] ^ partTwo[n % partTwo.length];

// Convert to a GUID (based on little-endian bytes)
const kid = createGUID(xorHash);

console.log(kid);

function createGUID(buffer:any) {
    // mind the reverse endianess for the first three parts
    return buffer.subarray(0, 4).reverse().toString('hex') + '-' +
        buffer.subarray(4, 6).reverse().toString('hex') + '-' +
        buffer.subarray(6, 8).reverse().toString('hex') + '-' +
        buffer.subarray(8, 10).toString('hex') + '-' +
        buffer.subarray(10, 16).toString('hex');
}

Java

public class App {
    private static String formatByteArrayToUUID(byte[] bytes) {
        StringBuilder formattedString = new StringBuilder();

        for (int i = 0; i < bytes.length; i++) {
            formattedString.append(String.format("%02x", bytes[i]));

            if (i == 3 || i == 5 || i == 7 || i == 9) {
                formattedString.append("-");
            }
        }

        return formattedString.toString();
    }

    public static void main(String[] args) throws NoSuchAlgorithmException {
        String inputParameters = "<see above>";
        byte[] baseToHashAsBytes = inputParameters.getBytes(StandardCharsets.UTF_8);

        // SHA256 hash
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] baseHashValue = digest.digest(baseToHashAsBytes);

        // XOR the first and second half of the base hash to make a 16 bytes hash
        byte[] hashPartOne = new byte[16];
        System.arraycopy(baseHashValue, 0, hashPartOne, 0, 16);

        byte[] hashPartTwo = new byte[16];
        System.arraycopy(baseHashValue, 16, hashPartTwo, 0, 16);

        byte[] xorHash = new byte[16];
        for (int i = 0; i < 16; i++) {
            xorHash[i] = (byte) (hashPartOne[i] ^ hashPartTwo[i]);
        }

        // Convert to a GUID (based on little-endian bytes)
        byte[] bytes_le = xorHash;
        int resultLength = 4 + (6 - 4) + (8 - 6) + (bytes_le.length - 8);
        byte[] result = new byte[resultLength];

        int index = 0;
        for (int i = 4 - 1; i >= 0; i--) {
            result[index++] = bytes_le[i];
        }
        for (int i = 6 - 1; i >= 4; i--) {
            result[index++] = bytes_le[i];
        }
        for (int i = 8 - 1; i >= 6; i--) {
            result[index++] = bytes_le[i];
        }
        for (int i = 8; i < bytes_le.length; i++) {
            result[index++] = bytes_le[i];
        }

        String formattedLowerCaseString = formatByteArrayToUUID(result);
        System.out.println(formattedLowerCaseString);
    }
}