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 rootcpix:CPIX
element for SPEKE v1 and thecontentId
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.
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.
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);
}
}