Shaka Packager SDK
Loading...
Searching...
No Matches
cpix_parser.cc
1// Copyright 2026 Google LLC. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd
6
7#include <packager/media/base/cpix_parser.h>
8
9#include <cstdint>
10#include <initializer_list>
11#include <optional>
12#include <string>
13#include <utility>
14#include <vector>
15
16#include <absl/log/check.h>
17#include <absl/strings/escaping.h>
18#include <absl/strings/numbers.h>
19#include <absl/strings/str_replace.h>
20#include <libxml/parser.h>
21#include <libxml/tree.h>
22
23#include <packager/macros/status.h>
24#include <packager/mpd/base/xml/scoped_xml_ptr.h>
25#include <packager/utils/hex_parser.h>
26
27namespace shaka {
28namespace media {
29namespace {
30
31// Matches on the element's local name only, so that documents are accepted
32// regardless of the namespace prefixes the producer chose.
33bool IsElement(xmlNodePtr node, const char* name) {
34 return node && node->type == XML_ELEMENT_NODE &&
35 xmlStrcmp(node->name, BAD_CAST name) == 0;
36}
37
38xmlNodePtr FindChildElement(xmlNodePtr node, const char* name) {
39 for (xmlNodePtr child = node->children; child; child = child->next) {
40 if (IsElement(child, name))
41 return child;
42 }
43 return nullptr;
44}
45
46std::optional<std::string> GetAttribute(xmlNodePtr node, const char* name) {
47 xml::scoped_xml_ptr<xmlChar> value(xmlGetProp(node, BAD_CAST name));
48 if (!value)
49 return std::nullopt;
50 return std::string(reinterpret_cast<const char*>(value.get()));
51}
52
53std::string GetContent(xmlNodePtr node) {
54 xml::scoped_xml_ptr<xmlChar> content(xmlNodeGetContent(node));
55 if (!content)
56 return "";
57 return std::string(reinterpret_cast<const char*>(content.get()));
58}
59
60Status GetRequiredAttribute(xmlNodePtr node,
61 const char* name,
62 const std::string& element_desc,
63 std::string* value) {
64 std::optional<std::string> attribute = GetAttribute(node, name);
65 if (!attribute) {
66 return Status(error::INVALID_ARGUMENT,
67 element_desc + " is missing the '" + name + "' attribute.");
68 }
69 *value = std::move(*attribute);
70 return Status::OK;
71}
72
73Status ParseUuid(const std::string& uuid,
74 const std::string& error_context,
75 std::vector<uint8_t>* bytes) {
76 const std::string hex = absl::StrReplaceAll(uuid, {{"-", ""}});
77 if (!ValidHexStringToBytes(hex, bytes) || bytes->size() != 16) {
78 return Status(error::INVALID_ARGUMENT,
79 error_context + " is not a valid UUID: " + uuid);
80 }
81 return Status::OK;
82}
83
84Status ParseBase64(const std::string& base64,
85 const std::string& error_context,
86 std::vector<uint8_t>* bytes) {
87 // absl::Base64Unescape skips whitespace, so pretty-printed documents with
88 // line breaks inside the value are handled.
89 std::string decoded;
90 if (!absl::Base64Unescape(base64, &decoded)) {
91 return Status(error::INVALID_ARGUMENT,
92 error_context + " is not valid base64: " + base64);
93 }
94 bytes->assign(decoded.begin(), decoded.end());
95 return Status::OK;
96}
97
98Status ParseEncryptedValue(xmlNodePtr node,
99 const std::string& error_context,
100 CpixEncryptedValue* encrypted_value) {
101 xmlNodePtr method = FindChildElement(node, "EncryptionMethod");
102 if (method)
103 encrypted_value->algorithm = GetAttribute(method, "Algorithm").value_or("");
104
105 xmlNodePtr cipher_data = FindChildElement(node, "CipherData");
106 xmlNodePtr cipher_value =
107 cipher_data ? FindChildElement(cipher_data, "CipherValue") : nullptr;
108 if (!cipher_value) {
109 return Status(error::INVALID_ARGUMENT,
110 "EncryptedValue of " + error_context +
111 " has no CipherData/CipherValue.");
112 }
113 return ParseBase64(GetContent(cipher_value),
114 "CipherValue of " + error_context,
115 &encrypted_value->cipher_value);
116}
117
118Status ParseContentKey(xmlNodePtr node, CpixContentKey* content_key) {
119 std::string kid;
120 RETURN_IF_ERROR(
121 GetRequiredAttribute(node, "kid", "ContentKey element", &kid));
122 RETURN_IF_ERROR(ParseUuid(kid, "ContentKey@kid", &content_key->key_id));
123
124 std::optional<std::string> explicit_iv = GetAttribute(node, "explicitIV");
125 if (explicit_iv) {
126 RETURN_IF_ERROR(ParseBase64(*explicit_iv, "explicitIV of ContentKey " + kid,
127 &content_key->iv));
128 }
129
130 content_key->common_encryption_scheme =
131 GetAttribute(node, "commonEncryptionScheme").value_or("");
132
133 xmlNodePtr data = FindChildElement(node, "Data");
134 xmlNodePtr secret = data ? FindChildElement(data, "Secret") : nullptr;
135 if (!secret) {
136 return Status(error::INVALID_ARGUMENT,
137 "ContentKey " + kid + " has no Data/Secret element.");
138 }
139 xmlNodePtr plain_value = FindChildElement(secret, "PlainValue");
140 xmlNodePtr encrypted_value = FindChildElement(secret, "EncryptedValue");
141 if (plain_value && encrypted_value) {
142 return Status(
143 error::INVALID_ARGUMENT,
144 "ContentKey " + kid + " has both a PlainValue and an EncryptedValue.");
145 }
146 if (encrypted_value) {
147 CpixEncryptedValue value;
148 RETURN_IF_ERROR(
149 ParseEncryptedValue(encrypted_value, "ContentKey " + kid, &value));
150 xmlNodePtr value_mac = FindChildElement(secret, "ValueMAC");
151 if (value_mac) {
152 RETURN_IF_ERROR(ParseBase64(GetContent(value_mac),
153 "ValueMAC of ContentKey " + kid,
154 &value.value_mac));
155 }
156 content_key->encrypted_key = std::move(value);
157 return Status::OK;
158 }
159 if (!plain_value) {
160 return Status(error::INVALID_ARGUMENT,
161 "ContentKey " + kid +
162 " has no Data/Secret/PlainValue or EncryptedValue.");
163 }
164 RETURN_IF_ERROR(ParseBase64(GetContent(plain_value),
165 "PlainValue of ContentKey " + kid,
166 &content_key->key));
167 return Status::OK;
168}
169
170Status ParseDeliveryData(xmlNodePtr node, CpixDeliveryData* delivery_data) {
171 xmlNodePtr document_key = FindChildElement(node, "DocumentKey");
172 xmlNodePtr data =
173 document_key ? FindChildElement(document_key, "Data") : nullptr;
174 xmlNodePtr secret = data ? FindChildElement(data, "Secret") : nullptr;
175 xmlNodePtr encrypted_value =
176 secret ? FindChildElement(secret, "EncryptedValue") : nullptr;
177 if (!encrypted_value) {
178 return Status(error::INVALID_ARGUMENT,
179 "DeliveryData has no "
180 "DocumentKey/Data/Secret/EncryptedValue.");
181 }
182 RETURN_IF_ERROR(ParseEncryptedValue(encrypted_value, "DocumentKey",
183 &delivery_data->document_key));
184
185 xmlNodePtr mac_method = FindChildElement(node, "MACMethod");
186 if (mac_method) {
187 delivery_data->mac_algorithm =
188 GetAttribute(mac_method, "Algorithm").value_or("");
189 if (delivery_data->mac_algorithm.empty()) {
190 return Status(error::INVALID_ARGUMENT,
191 "MACMethod is missing the 'Algorithm' attribute.");
192 }
193 xmlNodePtr key = FindChildElement(mac_method, "Key");
194 xmlNodePtr key_encrypted_value =
195 key ? FindChildElement(key, "EncryptedValue") : nullptr;
196 if (!key_encrypted_value) {
197 return Status(error::INVALID_ARGUMENT,
198 "MACMethod has no Key/EncryptedValue.");
199 }
200 RETURN_IF_ERROR(ParseEncryptedValue(key_encrypted_value, "MACMethod key",
201 &delivery_data->mac_key));
202 }
203 return Status::OK;
204}
205
206Status ParseDrmSystem(xmlNodePtr node, CpixDrmSystem* drm_system) {
207 std::string kid;
208 RETURN_IF_ERROR(GetRequiredAttribute(node, "kid", "DRMSystem element", &kid));
209 RETURN_IF_ERROR(ParseUuid(kid, "DRMSystem@kid", &drm_system->key_id));
210
211 std::string system_id;
212 RETURN_IF_ERROR(GetRequiredAttribute(
213 node, "systemId", "DRMSystem element for key " + kid, &system_id));
214 RETURN_IF_ERROR(
215 ParseUuid(system_id, "DRMSystem@systemId", &drm_system->system_id));
216
217 xmlNodePtr pssh = FindChildElement(node, "PSSH");
218 if (pssh) {
219 RETURN_IF_ERROR(ParseBase64(
220 GetContent(pssh), "PSSH of DRMSystem " + system_id + " for key " + kid,
221 &drm_system->pssh));
222 }
223 return Status::OK;
224}
225
226Status ParseUsageRule(xmlNodePtr node, CpixUsageRule* usage_rule) {
227 std::string kid;
228 RETURN_IF_ERROR(
229 GetRequiredAttribute(node, "kid", "ContentKeyUsageRule element", &kid));
230 RETURN_IF_ERROR(
231 ParseUuid(kid, "ContentKeyUsageRule@kid", &usage_rule->key_id));
232
233 usage_rule->intended_track_type =
234 GetAttribute(node, "intendedTrackType").value_or("");
235
236 // Usage rules narrow key usage with filter elements. Silently ignoring a
237 // filter (or an unsupported filter attribute) would apply the key more
238 // broadly than the document allows, so anything not understood is
239 // rejected.
240 auto reject_unsupported_attributes =
241 [&kid](xmlNodePtr filter, const char* filter_desc,
242 std::initializer_list<const char*> attributes) -> Status {
243 for (const char* attribute : attributes) {
244 if (GetAttribute(filter, attribute)) {
245 return Status(error::UNIMPLEMENTED,
246 "ContentKeyUsageRule for key " + kid + " contains " +
247 filter_desc + " with the '" + attribute +
248 "' attribute, which is not supported yet.");
249 }
250 }
251 return Status::OK;
252 };
253 for (xmlNodePtr child = node->children; child; child = child->next) {
254 if (child->type != XML_ELEMENT_NODE)
255 continue;
256 if (IsElement(child, "VideoFilter")) {
257 RETURN_IF_ERROR(reject_unsupported_attributes(
258 child, "a VideoFilter", {"hdr", "wcg", "minFps", "maxFps"}));
259 CpixVideoFilter video_filter;
260 std::optional<std::string> min_pixels = GetAttribute(child, "minPixels");
261 if (min_pixels &&
262 (!absl::SimpleAtoi(*min_pixels, &video_filter.min_pixels) ||
263 video_filter.min_pixels < 0)) {
264 return Status(error::INVALID_ARGUMENT,
265 "Invalid VideoFilter@minPixels for key " + kid + ": " +
266 *min_pixels);
267 }
268 std::optional<std::string> max_pixels = GetAttribute(child, "maxPixels");
269 if (max_pixels &&
270 (!absl::SimpleAtoi(*max_pixels, &video_filter.max_pixels) ||
271 video_filter.max_pixels < 0)) {
272 return Status(error::INVALID_ARGUMENT,
273 "Invalid VideoFilter@maxPixels for key " + kid + ": " +
274 *max_pixels);
275 }
276 usage_rule->video_filters.push_back(video_filter);
277 } else if (IsElement(child, "AudioFilter")) {
278 RETURN_IF_ERROR(reject_unsupported_attributes(
279 child, "an AudioFilter", {"minChannels", "maxChannels"}));
280 usage_rule->has_audio_filter = true;
281 } else {
282 return Status(
283 error::UNIMPLEMENTED,
284 "ContentKeyUsageRule for key " + kid + " contains a " +
285 reinterpret_cast<const char*>(child->name) +
286 " element, which is not supported yet. Only VideoFilter, "
287 "AudioFilter and the 'intendedTrackType' attribute are "
288 "supported.");
289 }
290 }
291
292 if (usage_rule->has_audio_filter && !usage_rule->video_filters.empty()) {
293 // Per the CPIX specification, filters of different types are combined
294 // with AND, so a rule with both an audio and a video filter can never
295 // match any stream.
296 return Status(error::INVALID_ARGUMENT,
297 "ContentKeyUsageRule for key " + kid +
298 " contains both an AudioFilter and a VideoFilter, so "
299 "it cannot match any stream.");
300 }
301 return Status::OK;
302}
303
304// Parses every |element_name| child of |list| with |parse| into |out|.
305template <typename T>
306Status ParseList(xmlNodePtr list,
307 const char* element_name,
308 Status (*parse)(xmlNodePtr, T*),
309 std::vector<T>* out) {
310 for (xmlNodePtr node = list->children; node; node = node->next) {
311 if (!IsElement(node, element_name))
312 continue;
313 T item;
314 RETURN_IF_ERROR(parse(node, &item));
315 out->push_back(std::move(item));
316 }
317 return Status::OK;
318}
319
320} // namespace
321
322Status ParseCpixDocument(const std::string& xml, CpixDocument* document) {
323 DCHECK(document);
324
325 xml::scoped_xml_ptr<xmlDoc> doc(xmlReadMemory(
326 xml.data(), static_cast<int>(xml.size()), /* URL= */ nullptr,
327 /* encoding= */ nullptr, XML_PARSE_NONET));
328 if (!doc) {
329 return Status(error::INVALID_ARGUMENT,
330 "Failed to parse the CPIX document as XML.");
331 }
332
333 xmlNodePtr root = xmlDocGetRootElement(doc.get());
334 if (!IsElement(root, "CPIX")) {
335 return Status(error::INVALID_ARGUMENT,
336 "The root element of a CPIX document must be CPIX.");
337 }
338
339 for (xmlNodePtr list = root->children; list; list = list->next) {
340 if (IsElement(list, "ContentKeyList")) {
341 RETURN_IF_ERROR(ParseList(list, "ContentKey", &ParseContentKey,
342 &document->content_keys));
343 } else if (IsElement(list, "DRMSystemList")) {
344 RETURN_IF_ERROR(ParseList(list, "DRMSystem", &ParseDrmSystem,
345 &document->drm_systems));
346 } else if (IsElement(list, "ContentKeyUsageRuleList")) {
347 RETURN_IF_ERROR(ParseList(list, "ContentKeyUsageRule", &ParseUsageRule,
348 &document->usage_rules));
349 } else if (IsElement(list, "DeliveryDataList")) {
350 RETURN_IF_ERROR(ParseList(list, "DeliveryData", &ParseDeliveryData,
351 &document->delivery_data));
352 }
353 // Other lists (ContentKeyPeriodList, UpdateHistory, Signature, ...) are
354 // not needed for packaging and are ignored.
355 }
356
357 if (document->content_keys.empty()) {
358 return Status(error::INVALID_ARGUMENT,
359 "The CPIX document contains no content keys.");
360 }
361 for (size_t i = 0; i < document->content_keys.size(); ++i) {
362 for (size_t j = i + 1; j < document->content_keys.size(); ++j) {
363 if (document->content_keys[i].key_id ==
364 document->content_keys[j].key_id) {
365 const std::vector<uint8_t>& key_id = document->content_keys[i].key_id;
366 return Status(
367 error::INVALID_ARGUMENT,
368 "Duplicate ContentKey kid " + absl::BytesToHexString(std::string(
369 key_id.begin(), key_id.end())));
370 }
371 }
372 }
373 return Status::OK;
374}
375
376} // namespace media
377} // namespace shaka
All the methods that are virtual are virtual for mocking.