| 1 | /* | 
|---|
| 2 | * Copyright (C) 2016 Metrological Group B.V. | 
|---|
| 3 | * Copyright (C) 2016 Igalia S.L. | 
|---|
| 4 | * | 
|---|
| 5 | * Redistribution and use in source and binary forms, with or without | 
|---|
| 6 | * modification, are permitted provided that the following conditions | 
|---|
| 7 | * are met: | 
|---|
| 8 | * | 
|---|
| 9 | * 1. Redistributions of source code must retain the above copyright | 
|---|
| 10 | *    notice, this list of conditions and the following disclaimer. | 
|---|
| 11 | * 2. Redistributions in binary form must reproduce the above | 
|---|
| 12 | *    copyright notice, this list of conditions and the following | 
|---|
| 13 | *    disclaimer in the documentation and/or other materials provided | 
|---|
| 14 | *    with the distribution. | 
|---|
| 15 | * | 
|---|
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 
|---|
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 
|---|
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
|---|
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | 
|---|
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
|---|
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | 
|---|
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
|---|
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
|---|
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 
|---|
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
|---|
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
|---|
| 27 | */ | 
|---|
| 28 |  | 
|---|
| 29 | #include "config.h" | 
|---|
| 30 | #include "CDMClearKey.h" | 
|---|
| 31 |  | 
|---|
| 32 | #if ENABLE(ENCRYPTED_MEDIA) | 
|---|
| 33 |  | 
|---|
| 34 | #include "CDMKeySystemConfiguration.h" | 
|---|
| 35 | #include "CDMRestrictions.h" | 
|---|
| 36 | #include "CDMSessionType.h" | 
|---|
| 37 | #include "SharedBuffer.h" | 
|---|
| 38 | #include <wtf/JSONValues.h> | 
|---|
| 39 | #include <wtf/MainThread.h> | 
|---|
| 40 | #include <wtf/NeverDestroyed.h> | 
|---|
| 41 | #include <wtf/text/Base64.h> | 
|---|
| 42 |  | 
|---|
| 43 | namespace WebCore { | 
|---|
| 44 |  | 
|---|
| 45 | // ClearKey CENC SystemID. | 
|---|
| 46 | // https://www.w3.org/TR/eme-initdata-cenc/#common-system | 
|---|
| 47 | const uint8_t clearKeyCencSystemId[] = { 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b }; | 
|---|
| 48 | const unsigned clearKeyCencSystemIdSize = sizeof(clearKeyCencSystemId); | 
|---|
| 49 | const unsigned keyIdSize = 16; | 
|---|
| 50 |  | 
|---|
| 51 | class ClearKeyState { | 
|---|
| 52 | using KeyStore = HashMap<String, Vector<CDMInstanceClearKey::Key>>; | 
|---|
| 53 |  | 
|---|
| 54 | public: | 
|---|
| 55 | static ClearKeyState& singleton(); | 
|---|
| 56 |  | 
|---|
| 57 | KeyStore& keys() { return m_keys; } | 
|---|
| 58 |  | 
|---|
| 59 | private: | 
|---|
| 60 | friend class NeverDestroyed<ClearKeyState>; | 
|---|
| 61 | ClearKeyState(); | 
|---|
| 62 | KeyStore m_keys; | 
|---|
| 63 | }; | 
|---|
| 64 |  | 
|---|
| 65 | ClearKeyState& ClearKeyState::singleton() | 
|---|
| 66 | { | 
|---|
| 67 | static NeverDestroyed<ClearKeyState> s_state; | 
|---|
| 68 | return s_state; | 
|---|
| 69 | } | 
|---|
| 70 |  | 
|---|
| 71 | ClearKeyState::ClearKeyState() = default; | 
|---|
| 72 |  | 
|---|
| 73 | static RefPtr<JSON::Object> parseJSONObject(const SharedBuffer& buffer) | 
|---|
| 74 | { | 
|---|
| 75 | // Fail on large buffers whose size doesn't fit into a 32-bit unsigned integer. | 
|---|
| 76 | size_t size = buffer.size(); | 
|---|
| 77 | if (size > std::numeric_limits<unsigned>::max()) | 
|---|
| 78 | return nullptr; | 
|---|
| 79 |  | 
|---|
| 80 | // Parse the buffer contents as JSON, returning the root object (if any). | 
|---|
| 81 | String json { buffer.data(), static_cast<unsigned>(size) }; | 
|---|
| 82 | RefPtr<JSON::Value> value; | 
|---|
| 83 | RefPtr<JSON::Object> object; | 
|---|
| 84 | if (!JSON::Value::parseJSON(json, value) || !value->asObject(object)) | 
|---|
| 85 | return nullptr; | 
|---|
| 86 |  | 
|---|
| 87 | return object; | 
|---|
| 88 | } | 
|---|
| 89 |  | 
|---|
| 90 | static Optional<Vector<CDMInstanceClearKey::Key>> parseLicenseFormat(const JSON::Object& root) | 
|---|
| 91 | { | 
|---|
| 92 | // If the 'keys' key is present in the root object, parse the JSON further | 
|---|
| 93 | // according to the specified 'license' format. | 
|---|
| 94 | auto it = root.find( "keys"); | 
|---|
| 95 | if (it == root.end()) | 
|---|
| 96 | return WTF::nullopt; | 
|---|
| 97 |  | 
|---|
| 98 | // Retrieve the keys array. | 
|---|
| 99 | RefPtr<JSON::Array> keysArray; | 
|---|
| 100 | if (!it->value->asArray(keysArray)) | 
|---|
| 101 | return WTF::nullopt; | 
|---|
| 102 |  | 
|---|
| 103 | Vector<CDMInstanceClearKey::Key> decodedKeys; | 
|---|
| 104 | bool validFormat = std::all_of(keysArray->begin(), keysArray->end(), | 
|---|
| 105 | [&decodedKeys] (const auto& value) { | 
|---|
| 106 | RefPtr<JSON::Object> keyObject; | 
|---|
| 107 | if (!value->asObject(keyObject)) | 
|---|
| 108 | return false; | 
|---|
| 109 |  | 
|---|
| 110 | String keyType; | 
|---|
| 111 | if (!keyObject->getString( "kty", keyType) || !equalLettersIgnoringASCIICase(keyType, "oct")) | 
|---|
| 112 | return false; | 
|---|
| 113 |  | 
|---|
| 114 | String keyID, keyValue; | 
|---|
| 115 | if (!keyObject->getString( "kid", keyID) || !keyObject->getString( "k", keyValue)) | 
|---|
| 116 | return false; | 
|---|
| 117 |  | 
|---|
| 118 | Vector<char> keyIDData, keyValueData; | 
|---|
| 119 | if (!WTF::base64URLDecode(keyID, { keyIDData }) || !WTF::base64URLDecode(keyValue, { keyValueData })) | 
|---|
| 120 | return false; | 
|---|
| 121 |  | 
|---|
| 122 | decodedKeys.append({ CDMInstanceSession::KeyStatus::Usable, SharedBuffer::create(WTFMove(keyIDData)), SharedBuffer::create(WTFMove(keyValueData)) }); | 
|---|
| 123 | return true; | 
|---|
| 124 | }); | 
|---|
| 125 | if (!validFormat) | 
|---|
| 126 | return WTF::nullopt; | 
|---|
| 127 | return decodedKeys; | 
|---|
| 128 | } | 
|---|
| 129 |  | 
|---|
| 130 | static bool parseLicenseReleaseAcknowledgementFormat(const JSON::Object& root) | 
|---|
| 131 | { | 
|---|
| 132 | // If the 'kids' key is present in the root object, parse the JSON further | 
|---|
| 133 | // according to the specified 'license release acknowledgement' format. | 
|---|
| 134 | auto it = root.find( "kids"); | 
|---|
| 135 | if (it == root.end()) | 
|---|
| 136 | return false; | 
|---|
| 137 |  | 
|---|
| 138 | // Retrieve the kids array. | 
|---|
| 139 | RefPtr<JSON::Array> kidsArray; | 
|---|
| 140 | if (!it->value->asArray(kidsArray)) | 
|---|
| 141 | return false; | 
|---|
| 142 |  | 
|---|
| 143 | // FIXME: Return the key IDs and validate them. | 
|---|
| 144 | return true; | 
|---|
| 145 | } | 
|---|
| 146 |  | 
|---|
| 147 | // https://www.w3.org/TR/eme-initdata-cenc/#common-system | 
|---|
| 148 | // 4.1 Definition | 
|---|
| 149 | // The SystemID is 1077efec-c0b2-4d02-ace3-3c1e52e2fb4b. | 
|---|
| 150 | // The PSSH box format is as follows. It follows version 1 of the 'pssh' box as defined in [CENC]. | 
|---|
| 151 | // pssh = [ | 
|---|
| 152 | // 0x00, 0x00, 0x00, 0x4c, 0x70, 0x73, 0x73, 0x68, // BMFF box header (76 bytes, 'pssh') | 
|---|
| 153 | // 0x01, 0x00, 0x00, 0x00,                         // Full box header (version = 1, flags = 0) | 
|---|
| 154 | // 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, // SystemID | 
|---|
| 155 | // 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, | 
|---|
| 156 | // 0x00, 0x00, 0x00, 0x02,                         // KidCount (2) | 
|---|
| 157 | // 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // First KID ("0123456789012345") | 
|---|
| 158 | // 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, | 
|---|
| 159 | // 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, // Second KID ("ABCDEFGHIJKLMNOP") | 
|---|
| 160 | // 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, | 
|---|
| 161 | // 0x00, 0x00, 0x00, 0x00,                         // Size of Data (0) | 
|---|
| 162 | // ]; | 
|---|
| 163 |  | 
|---|
| 164 | // This function extracts the KeyIds count and the location of the first KeyId in initData buffer. | 
|---|
| 165 | static std::pair<unsigned, unsigned> (const SharedBuffer& initData) | 
|---|
| 166 | { | 
|---|
| 167 | std::pair<unsigned, unsigned> keyIdsMap(0, 0); | 
|---|
| 168 |  | 
|---|
| 169 | // Check the initData size. | 
|---|
| 170 | if (initData.isEmpty() || initData.size() > std::numeric_limits<unsigned>::max()) | 
|---|
| 171 | return keyIdsMap; | 
|---|
| 172 |  | 
|---|
| 173 | const char* data = initData.data(); | 
|---|
| 174 | unsigned initDataSize = initData.size(); | 
|---|
| 175 | unsigned index = 0; | 
|---|
| 176 | unsigned psshSize = 0; | 
|---|
| 177 |  | 
|---|
| 178 | // Search in the concatenated or the simple InitData, the ClearKey PSSH. | 
|---|
| 179 | bool foundPssh = false; | 
|---|
| 180 | while (true) { | 
|---|
| 181 |  | 
|---|
| 182 | // Check the overflow InitData. | 
|---|
| 183 | if (index + 12 + clearKeyCencSystemIdSize >= initDataSize) | 
|---|
| 184 | return keyIdsMap; | 
|---|
| 185 |  | 
|---|
| 186 | psshSize = data[index + 2] * 256 + data[index + 3]; | 
|---|
| 187 |  | 
|---|
| 188 | // Check the pssh size | 
|---|
| 189 | if (!psshSize) | 
|---|
| 190 | return keyIdsMap; | 
|---|
| 191 |  | 
|---|
| 192 | // 12 = BMFF box header + Full box header. | 
|---|
| 193 | if (!memcmp(&data[index + 12], clearKeyCencSystemId, clearKeyCencSystemIdSize)) { | 
|---|
| 194 | foundPssh = true; | 
|---|
| 195 | break; | 
|---|
| 196 | } | 
|---|
| 197 | index += psshSize; | 
|---|
| 198 | } | 
|---|
| 199 |  | 
|---|
| 200 | // Check if the InitData contains the ClearKey PSSH. | 
|---|
| 201 | if (!foundPssh) | 
|---|
| 202 | return keyIdsMap; | 
|---|
| 203 |  | 
|---|
| 204 | index += (12 + clearKeyCencSystemIdSize); // 12 (BMFF box header + Full box header) + SystemID size. | 
|---|
| 205 |  | 
|---|
| 206 | // Check the overflow. | 
|---|
| 207 | if (index + 3 >= initDataSize) | 
|---|
| 208 | return keyIdsMap; | 
|---|
| 209 |  | 
|---|
| 210 | keyIdsMap.first = data[index + 3]; // Read the KeyIdsCount. | 
|---|
| 211 | index += 4; // KeyIdsCount size. | 
|---|
| 212 |  | 
|---|
| 213 | // Check the overflow. | 
|---|
| 214 | if ((index + (keyIdsMap.first * keyIdSize)) >= initDataSize) | 
|---|
| 215 | return keyIdsMap; | 
|---|
| 216 |  | 
|---|
| 217 | keyIdsMap.second = index; // The location of the first KeyId in initData. | 
|---|
| 218 |  | 
|---|
| 219 | return keyIdsMap; | 
|---|
| 220 | } | 
|---|
| 221 |  | 
|---|
| 222 | // This function checks if the initData sharedBuffer is a valid CENC initData. | 
|---|
| 223 | static bool isCencInitData(const SharedBuffer& initData) | 
|---|
| 224 | { | 
|---|
| 225 | std::pair<unsigned, unsigned> keyIdsMap = extractKeyidsLocationFromCencInitData(initData); | 
|---|
| 226 | return ((keyIdsMap.first) && (keyIdsMap.second)); | 
|---|
| 227 | } | 
|---|
| 228 |  | 
|---|
| 229 | static Ref<SharedBuffer> (const SharedBuffer& initData) | 
|---|
| 230 | { | 
|---|
| 231 | Ref<SharedBuffer> keyIds = SharedBuffer::create(); | 
|---|
| 232 |  | 
|---|
| 233 | std::pair<unsigned, unsigned> keyIdsMap = extractKeyidsLocationFromCencInitData(initData); | 
|---|
| 234 | unsigned keyIdCount = keyIdsMap.first; | 
|---|
| 235 | unsigned index = keyIdsMap.second; | 
|---|
| 236 |  | 
|---|
| 237 | // Check if initData is a valid CENC initData. | 
|---|
| 238 | if (!keyIdCount || !index) | 
|---|
| 239 | return keyIds; | 
|---|
| 240 |  | 
|---|
| 241 | const char* data = initData.data(); | 
|---|
| 242 |  | 
|---|
| 243 | auto object = JSON::Object::create(); | 
|---|
| 244 | auto keyIdsArray = JSON::Array::create(); | 
|---|
| 245 |  | 
|---|
| 246 | // Read the KeyId | 
|---|
| 247 | // 9.1.3 License Request Format | 
|---|
| 248 | // This section describes the format of the license request provided to the application via the message attribute of the message event. | 
|---|
| 249 | // The format is a JSON object containing the following members: | 
|---|
| 250 | // "kids" | 
|---|
| 251 | // An array of key IDs. Each element of the array is the base64url encoding of the octet sequence containing the key ID value. | 
|---|
| 252 | for (unsigned i = 0; i < keyIdCount; i++) { | 
|---|
| 253 | String keyId = WTF::base64URLEncode(&data[index], keyIdSize); | 
|---|
| 254 | keyIdsArray->pushString(keyId); | 
|---|
| 255 | index += keyIdSize; | 
|---|
| 256 | } | 
|---|
| 257 |  | 
|---|
| 258 | object->setArray( "kids", WTFMove(keyIdsArray)); | 
|---|
| 259 | CString jsonData = object->toJSONString().utf8(); | 
|---|
| 260 | keyIds->append(jsonData.data(), jsonData.length()); | 
|---|
| 261 | return keyIds; | 
|---|
| 262 | } | 
|---|
| 263 |  | 
|---|
| 264 | static Ref<SharedBuffer> (const SharedBuffer& initData) | 
|---|
| 265 | { | 
|---|
| 266 | Ref<SharedBuffer> keyIds = SharedBuffer::create(); | 
|---|
| 267 |  | 
|---|
| 268 | // Check if initData is a valid WebM initData. | 
|---|
| 269 | if (initData.isEmpty() || initData.size() > std::numeric_limits<unsigned>::max()) | 
|---|
| 270 | return keyIds; | 
|---|
| 271 |  | 
|---|
| 272 | auto object = JSON::Object::create(); | 
|---|
| 273 | auto keyIdsArray = JSON::Array::create(); | 
|---|
| 274 |  | 
|---|
| 275 | // Read the KeyId | 
|---|
| 276 | // 9.1.3 License Request Format | 
|---|
| 277 | // This section describes the format of the license request provided to the application via the message attribute of the message event. | 
|---|
| 278 | // The format is a JSON object containing the following members: | 
|---|
| 279 | // "kids" | 
|---|
| 280 | // An array of key IDs. Each element of the array is the base64url encoding of the octet sequence containing the key ID value. | 
|---|
| 281 | String keyId = WTF::base64URLEncode(initData.data(), initData.size()); | 
|---|
| 282 | keyIdsArray->pushString(keyId); | 
|---|
| 283 |  | 
|---|
| 284 | object->setArray( "kids", WTFMove(keyIdsArray)); | 
|---|
| 285 | CString jsonData = object->toJSONString().utf8(); | 
|---|
| 286 | keyIds->append(jsonData.data(), jsonData.length()); | 
|---|
| 287 | return keyIds; | 
|---|
| 288 | } | 
|---|
| 289 |  | 
|---|
| 290 | CDMFactoryClearKey& CDMFactoryClearKey::singleton() | 
|---|
| 291 | { | 
|---|
| 292 | static NeverDestroyed<CDMFactoryClearKey> s_factory; | 
|---|
| 293 | return s_factory; | 
|---|
| 294 | } | 
|---|
| 295 |  | 
|---|
| 296 | CDMFactoryClearKey::CDMFactoryClearKey() = default; | 
|---|
| 297 | CDMFactoryClearKey::~CDMFactoryClearKey() = default; | 
|---|
| 298 |  | 
|---|
| 299 | std::unique_ptr<CDMPrivate> CDMFactoryClearKey::createCDM(const String& keySystem) | 
|---|
| 300 | { | 
|---|
| 301 | #ifdef NDEBUG | 
|---|
| 302 | UNUSED_PARAM(keySystem); | 
|---|
| 303 | #else | 
|---|
| 304 | ASSERT(supportsKeySystem(keySystem)); | 
|---|
| 305 | #endif | 
|---|
| 306 | return std::make_unique<CDMPrivateClearKey>(); | 
|---|
| 307 | } | 
|---|
| 308 |  | 
|---|
| 309 | bool CDMFactoryClearKey::supportsKeySystem(const String& keySystem) | 
|---|
| 310 | { | 
|---|
| 311 | // `org.w3.clearkey` is the only supported key system. | 
|---|
| 312 | return equalLettersIgnoringASCIICase(keySystem, "org.w3.clearkey"); | 
|---|
| 313 | } | 
|---|
| 314 |  | 
|---|
| 315 | CDMPrivateClearKey::CDMPrivateClearKey() = default; | 
|---|
| 316 | CDMPrivateClearKey::~CDMPrivateClearKey() = default; | 
|---|
| 317 |  | 
|---|
| 318 | bool CDMPrivateClearKey::supportsInitDataType(const AtomicString& initDataType) const | 
|---|
| 319 | { | 
|---|
| 320 | // `keyids` and 'cenc' are the only supported init data type. | 
|---|
| 321 | return (equalLettersIgnoringASCIICase(initDataType, "keyids") || equalLettersIgnoringASCIICase(initDataType, "cenc") || equalLettersIgnoringASCIICase(initDataType, "webm")); | 
|---|
| 322 | } | 
|---|
| 323 |  | 
|---|
| 324 | static bool containsPersistentLicenseType(const Vector<CDMSessionType>& types) | 
|---|
| 325 | { | 
|---|
| 326 | return std::any_of(types.begin(), types.end(), | 
|---|
| 327 | [] (auto& sessionType) { return sessionType == CDMSessionType::PersistentLicense; }); | 
|---|
| 328 | } | 
|---|
| 329 |  | 
|---|
| 330 | bool CDMPrivateClearKey::supportsConfiguration(const CDMKeySystemConfiguration& configuration) const | 
|---|
| 331 | { | 
|---|
| 332 | // Reject any configuration that marks distinctive identifier as required. | 
|---|
| 333 | if (configuration.distinctiveIdentifier == CDMRequirement::Required) | 
|---|
| 334 | return false; | 
|---|
| 335 |  | 
|---|
| 336 | // Reject any configuration that marks persistent state as required, unless | 
|---|
| 337 | // the 'persistent-license' session type has to be supported. | 
|---|
| 338 | if (configuration.persistentState == CDMRequirement::Required && !containsPersistentLicenseType(configuration.sessionTypes)) | 
|---|
| 339 | return false; | 
|---|
| 340 |  | 
|---|
| 341 | return true; | 
|---|
| 342 | } | 
|---|
| 343 |  | 
|---|
| 344 | bool CDMPrivateClearKey::supportsConfigurationWithRestrictions(const CDMKeySystemConfiguration& configuration, const CDMRestrictions& restrictions) const | 
|---|
| 345 | { | 
|---|
| 346 | // Reject any configuration that marks distincitive identifier as required, or that marks | 
|---|
| 347 | // distinctive identifier as optional even when restrictions mark it as denied. | 
|---|
| 348 | if ((configuration.distinctiveIdentifier == CDMRequirement::Optional && restrictions.distinctiveIdentifierDenied) | 
|---|
| 349 | || configuration.distinctiveIdentifier == CDMRequirement::Required) | 
|---|
| 350 | return false; | 
|---|
| 351 |  | 
|---|
| 352 | // Reject any configuration that marks persistent state as optional even when | 
|---|
| 353 | // restrictions mark it as denied. | 
|---|
| 354 | if (configuration.persistentState == CDMRequirement::Optional && restrictions.persistentStateDenied) | 
|---|
| 355 | return false; | 
|---|
| 356 |  | 
|---|
| 357 | // Reject any configuration that marks persistent state as required, unless | 
|---|
| 358 | // the 'persistent-license' session type has to be supported. | 
|---|
| 359 | if (configuration.persistentState == CDMRequirement::Required && !containsPersistentLicenseType(configuration.sessionTypes)) | 
|---|
| 360 | return false; | 
|---|
| 361 |  | 
|---|
| 362 | return true; | 
|---|
| 363 | } | 
|---|
| 364 |  | 
|---|
| 365 | bool CDMPrivateClearKey::supportsSessionTypeWithConfiguration(CDMSessionType& sessionType, const CDMKeySystemConfiguration& configuration) const | 
|---|
| 366 | { | 
|---|
| 367 | // Only support the 'temporary' and 'persistent-license' session types. | 
|---|
| 368 | if (sessionType != CDMSessionType::Temporary && sessionType != CDMSessionType::PersistentLicense) | 
|---|
| 369 | return false; | 
|---|
| 370 | return supportsConfiguration(configuration); | 
|---|
| 371 | } | 
|---|
| 372 |  | 
|---|
| 373 | bool CDMPrivateClearKey::supportsRobustness(const String& robustness) const | 
|---|
| 374 | { | 
|---|
| 375 | // Only empty `robustness` string is supported. | 
|---|
| 376 | return robustness.isEmpty(); | 
|---|
| 377 | } | 
|---|
| 378 |  | 
|---|
| 379 | CDMRequirement CDMPrivateClearKey::distinctiveIdentifiersRequirement(const CDMKeySystemConfiguration&, const CDMRestrictions& restrictions) const | 
|---|
| 380 | { | 
|---|
| 381 | // Distinctive identifier is not allowed if it's been denied, otherwise it's optional. | 
|---|
| 382 | if (restrictions.distinctiveIdentifierDenied) | 
|---|
| 383 | return CDMRequirement::NotAllowed; | 
|---|
| 384 | return CDMRequirement::Optional; | 
|---|
| 385 | } | 
|---|
| 386 |  | 
|---|
| 387 | CDMRequirement CDMPrivateClearKey::persistentStateRequirement(const CDMKeySystemConfiguration&, const CDMRestrictions& restrictions) const | 
|---|
| 388 | { | 
|---|
| 389 | // Persistent state is not allowed if it's been denied, otherwise it's optional. | 
|---|
| 390 | if (restrictions.persistentStateDenied) | 
|---|
| 391 | return CDMRequirement::NotAllowed; | 
|---|
| 392 | return CDMRequirement::Optional; | 
|---|
| 393 | } | 
|---|
| 394 |  | 
|---|
| 395 | bool CDMPrivateClearKey::distinctiveIdentifiersAreUniquePerOriginAndClearable(const CDMKeySystemConfiguration&) const | 
|---|
| 396 | { | 
|---|
| 397 | return false; | 
|---|
| 398 | } | 
|---|
| 399 |  | 
|---|
| 400 | RefPtr<CDMInstance> CDMPrivateClearKey::createInstance() | 
|---|
| 401 | { | 
|---|
| 402 | return adoptRef(new CDMInstanceClearKey); | 
|---|
| 403 | } | 
|---|
| 404 |  | 
|---|
| 405 | void CDMPrivateClearKey::loadAndInitialize() | 
|---|
| 406 | { | 
|---|
| 407 | // No-op. | 
|---|
| 408 | } | 
|---|
| 409 |  | 
|---|
| 410 | bool CDMPrivateClearKey::supportsServerCertificates() const | 
|---|
| 411 | { | 
|---|
| 412 | // Server certificates are not supported. | 
|---|
| 413 | return false; | 
|---|
| 414 | } | 
|---|
| 415 |  | 
|---|
| 416 | bool CDMPrivateClearKey::supportsSessions() const | 
|---|
| 417 | { | 
|---|
| 418 | // Sessions are supported. | 
|---|
| 419 | return true; | 
|---|
| 420 | } | 
|---|
| 421 |  | 
|---|
| 422 | bool CDMPrivateClearKey::supportsInitData(const AtomicString& initDataType, const SharedBuffer& initData) const | 
|---|
| 423 | { | 
|---|
| 424 | // Validate the initData buffer as an JSON object in keyids case. | 
|---|
| 425 | if (equalLettersIgnoringASCIICase(initDataType, "keyids") && parseJSONObject(initData)) | 
|---|
| 426 | return true; | 
|---|
| 427 |  | 
|---|
| 428 | // Validate the initData buffer as CENC initData. | 
|---|
| 429 | if (equalLettersIgnoringASCIICase(initDataType, "cenc") && isCencInitData(initData)) | 
|---|
| 430 | return true; | 
|---|
| 431 |  | 
|---|
| 432 | // Validate the initData buffer as WebM initData. | 
|---|
| 433 | if (equalLettersIgnoringASCIICase(initDataType, "webm") && !initData.isEmpty()) | 
|---|
| 434 | return true; | 
|---|
| 435 |  | 
|---|
| 436 | return false; | 
|---|
| 437 | } | 
|---|
| 438 |  | 
|---|
| 439 | RefPtr<SharedBuffer> CDMPrivateClearKey::sanitizeResponse(const SharedBuffer& response) const | 
|---|
| 440 | { | 
|---|
| 441 | // Validate the response buffer as an JSON object. | 
|---|
| 442 | if (!parseJSONObject(response)) | 
|---|
| 443 | return nullptr; | 
|---|
| 444 |  | 
|---|
| 445 | return response.copy(); | 
|---|
| 446 | } | 
|---|
| 447 |  | 
|---|
| 448 | Optional<String> CDMPrivateClearKey::sanitizeSessionId(const String& sessionId) const | 
|---|
| 449 | { | 
|---|
| 450 | // Validate the session ID string as an 32-bit integer. | 
|---|
| 451 | bool ok; | 
|---|
| 452 | sessionId.toUIntStrict(&ok); | 
|---|
| 453 | if (!ok) | 
|---|
| 454 | return WTF::nullopt; | 
|---|
| 455 | return sessionId; | 
|---|
| 456 | } | 
|---|
| 457 |  | 
|---|
| 458 | CDMInstanceClearKey::CDMInstanceClearKey() | 
|---|
| 459 | { | 
|---|
| 460 | } | 
|---|
| 461 |  | 
|---|
| 462 | CDMInstanceClearKey::~CDMInstanceClearKey() = default; | 
|---|
| 463 |  | 
|---|
| 464 | CDMInstance::SuccessValue CDMInstanceClearKey::initializeWithConfiguration(const CDMKeySystemConfiguration&) | 
|---|
| 465 | { | 
|---|
| 466 | // No-op. | 
|---|
| 467 | return Succeeded; | 
|---|
| 468 | } | 
|---|
| 469 |  | 
|---|
| 470 | CDMInstance::SuccessValue CDMInstanceClearKey::setDistinctiveIdentifiersAllowed(bool allowed) | 
|---|
| 471 | { | 
|---|
| 472 | // Reject setting distinctive identifiers as allowed. | 
|---|
| 473 | return !allowed ? Succeeded : Failed; | 
|---|
| 474 | } | 
|---|
| 475 |  | 
|---|
| 476 | CDMInstance::SuccessValue CDMInstanceClearKey::setPersistentStateAllowed(bool allowed) | 
|---|
| 477 | { | 
|---|
| 478 | // Reject setting persistent state as allowed. | 
|---|
| 479 | return !allowed ? Succeeded : Failed; | 
|---|
| 480 | } | 
|---|
| 481 |  | 
|---|
| 482 | CDMInstance::SuccessValue CDMInstanceClearKey::setServerCertificate(Ref<SharedBuffer>&&) | 
|---|
| 483 | { | 
|---|
| 484 | // Reject setting any server certificate. | 
|---|
| 485 | return Failed; | 
|---|
| 486 | } | 
|---|
| 487 |  | 
|---|
| 488 | CDMInstance::SuccessValue CDMInstanceClearKey::setStorageDirectory(const String& storageDirectory) | 
|---|
| 489 | { | 
|---|
| 490 | // Reject any persistent state storage. | 
|---|
| 491 | return storageDirectory.isEmpty() ? Succeeded : Failed; | 
|---|
| 492 | } | 
|---|
| 493 |  | 
|---|
| 494 | const String& CDMInstanceClearKey::keySystem() const | 
|---|
| 495 | { | 
|---|
| 496 | static const NeverDestroyed<String> s_keySystem { MAKE_STATIC_STRING_IMPL( "org.w3.clearkey") }; | 
|---|
| 497 |  | 
|---|
| 498 | return s_keySystem; | 
|---|
| 499 | } | 
|---|
| 500 |  | 
|---|
| 501 | RefPtr<CDMInstanceSession> CDMInstanceClearKey::createSession() | 
|---|
| 502 | { | 
|---|
| 503 | return adoptRef(new CDMInstanceSessionClearKey()); | 
|---|
| 504 | } | 
|---|
| 505 |  | 
|---|
| 506 | const Vector<CDMInstanceClearKey::Key> CDMInstanceClearKey::keys() const | 
|---|
| 507 | { | 
|---|
| 508 | // Return the keys of all sessions. | 
|---|
| 509 | Vector<CDMInstanceClearKey::Key> allKeys { }; | 
|---|
| 510 | auto locker = holdLock(m_keysMutex); | 
|---|
| 511 | size_t initialCapacity = 0; | 
|---|
| 512 | for (auto& key : ClearKeyState::singleton().keys().values()) | 
|---|
| 513 | initialCapacity += key.size(); | 
|---|
| 514 | allKeys.reserveInitialCapacity(initialCapacity); | 
|---|
| 515 |  | 
|---|
| 516 | for (auto& key : ClearKeyState::singleton().keys().values()) | 
|---|
| 517 | allKeys.appendVector(key); | 
|---|
| 518 |  | 
|---|
| 519 | return allKeys; | 
|---|
| 520 | } | 
|---|
| 521 |  | 
|---|
| 522 | void CDMInstanceSessionClearKey::requestLicense(LicenseType, const AtomicString& initDataType, Ref<SharedBuffer>&& initData, LicenseCallback&& callback) | 
|---|
| 523 | { | 
|---|
| 524 | static uint32_t s_sessionIdValue = 0; | 
|---|
| 525 | ++s_sessionIdValue; | 
|---|
| 526 |  | 
|---|
| 527 | if (equalLettersIgnoringASCIICase(initDataType, "cenc")) | 
|---|
| 528 | initData = extractKeyidsFromCencInitData(initData.get()); | 
|---|
| 529 |  | 
|---|
| 530 | if (equalLettersIgnoringASCIICase(initDataType, "webm")) | 
|---|
| 531 | initData = extractKeyIdFromWebMInitData(initData.get()); | 
|---|
| 532 |  | 
|---|
| 533 | callOnMainThread( | 
|---|
| 534 | [weakThis = makeWeakPtr(*this), callback = WTFMove(callback), initData = WTFMove(initData), sessionIdValue = s_sessionIdValue]() mutable { | 
|---|
| 535 | if (!weakThis) | 
|---|
| 536 | return; | 
|---|
| 537 |  | 
|---|
| 538 | callback(WTFMove(initData), String::number(sessionIdValue), false, Succeeded); | 
|---|
| 539 | }); | 
|---|
| 540 | } | 
|---|
| 541 |  | 
|---|
| 542 | void CDMInstanceSessionClearKey::updateLicense(const String& sessionId, LicenseType, const SharedBuffer& response, LicenseUpdateCallback&& callback) | 
|---|
| 543 | { | 
|---|
| 544 | // Use a helper functor that schedules the callback dispatch, avoiding | 
|---|
| 545 | // duplicated callOnMainThread() calls. | 
|---|
| 546 | auto dispatchCallback = | 
|---|
| 547 | [this, &callback](bool sessionWasClosed, Optional<KeyStatusVector>&& changedKeys, SuccessValue succeeded) { | 
|---|
| 548 | callOnMainThread( | 
|---|
| 549 | [weakThis = makeWeakPtr(*this), callback = WTFMove(callback), sessionWasClosed, changedKeys = WTFMove(changedKeys), succeeded] () mutable { | 
|---|
| 550 | if (!weakThis) | 
|---|
| 551 | return; | 
|---|
| 552 |  | 
|---|
| 553 | callback(sessionWasClosed, WTFMove(changedKeys), WTF::nullopt, WTF::nullopt, succeeded); | 
|---|
| 554 | }); | 
|---|
| 555 | }; | 
|---|
| 556 |  | 
|---|
| 557 | // Parse the response buffer as an JSON object. | 
|---|
| 558 | RefPtr<JSON::Object> root = parseJSONObject(response); | 
|---|
| 559 | if (!root) { | 
|---|
| 560 | dispatchCallback(false, WTF::nullopt, SuccessValue::Failed); | 
|---|
| 561 | return; | 
|---|
| 562 | } | 
|---|
| 563 |  | 
|---|
| 564 | // Parse the response using 'license' formatting, if possible. | 
|---|
| 565 | if (auto decodedKeys = parseLicenseFormat(*root)) { | 
|---|
| 566 | // Retrieve the target Vector of Key objects for this session. | 
|---|
| 567 | auto& keyVector = ClearKeyState::singleton().keys().ensure(sessionId, [] { return Vector<CDMInstanceClearKey::Key> { }; }).iterator->value; | 
|---|
| 568 |  | 
|---|
| 569 | // For each decoded key, find an existing item for the decoded key's ID. If none exist, | 
|---|
| 570 | // the key is decoded. Otherwise, the key is updated in case there's a mismatch between | 
|---|
| 571 | // the size or data of the existing and proposed key. | 
|---|
| 572 | bool keysChanged = false; | 
|---|
| 573 | for (auto& key : *decodedKeys) { | 
|---|
| 574 | auto it = std::find_if(keyVector.begin(), keyVector.end(), | 
|---|
| 575 | [&key] (const CDMInstanceClearKey::Key& containedKey) { | 
|---|
| 576 | return containedKey.keyIDData->size() == key.keyIDData->size() | 
|---|
| 577 | && !std::memcmp(containedKey.keyIDData->data(), key.keyIDData->data(), containedKey.keyIDData->size()); | 
|---|
| 578 | }); | 
|---|
| 579 | if (it != keyVector.end()) { | 
|---|
| 580 | auto& existingKey = it->keyValueData; | 
|---|
| 581 | auto& proposedKey = key.keyValueData; | 
|---|
| 582 |  | 
|---|
| 583 | // Update the existing Key if it differs from the proposed key in key value. | 
|---|
| 584 | if (existingKey->size() != proposedKey->size() || std::memcmp(existingKey->data(), proposedKey->data(), existingKey->size())) { | 
|---|
| 585 | *it = WTFMove(key); | 
|---|
| 586 | keysChanged = true; | 
|---|
| 587 | } | 
|---|
| 588 | } else { | 
|---|
| 589 | // In case a Key for this key ID doesn't exist yet, append the new one to keyVector. | 
|---|
| 590 | keyVector.append(WTFMove(key)); | 
|---|
| 591 | keysChanged = true; | 
|---|
| 592 | } | 
|---|
| 593 | } | 
|---|
| 594 |  | 
|---|
| 595 | // In case of changed keys, we have to provide a KeyStatusVector of all the keys for | 
|---|
| 596 | // this session. | 
|---|
| 597 | Optional<KeyStatusVector> changedKeys; | 
|---|
| 598 | if (keysChanged) { | 
|---|
| 599 | // First a helper Vector is constructed, cotaining pairs of SharedBuffer RefPtrs | 
|---|
| 600 | // representint key ID data, and the corresponding key statuses. | 
|---|
| 601 | // We can't use KeyStatusVector here because this Vector has to be sorted, which | 
|---|
| 602 | // is not possible to do on Ref<> objects. | 
|---|
| 603 | Vector<std::pair<RefPtr<SharedBuffer>, KeyStatus>> keys; | 
|---|
| 604 | keys.reserveInitialCapacity(keyVector.size()); | 
|---|
| 605 | for (auto& it : keyVector) | 
|---|
| 606 | keys.uncheckedAppend(std::pair<RefPtr<SharedBuffer>, KeyStatus> { it.keyIDData, it.status }); | 
|---|
| 607 |  | 
|---|
| 608 | // Sort first by size, second by data. | 
|---|
| 609 | std::sort(keys.begin(), keys.end(), | 
|---|
| 610 | [] (const auto& a, const auto& b) { | 
|---|
| 611 | if (a.first->size() != b.first->size()) | 
|---|
| 612 | return a.first->size() < b.first->size(); | 
|---|
| 613 |  | 
|---|
| 614 | return std::memcmp(a.first->data(), b.first->data(), a.first->size()) < 0; | 
|---|
| 615 | }); | 
|---|
| 616 |  | 
|---|
| 617 | // Finally construct the mirroring KeyStatusVector object and move it into the | 
|---|
| 618 | // Optional<> object that will be passed to the callback. | 
|---|
| 619 | KeyStatusVector keyStatusVector; | 
|---|
| 620 | keyStatusVector.reserveInitialCapacity(keys.size()); | 
|---|
| 621 | for (auto& it : keys) | 
|---|
| 622 | keyStatusVector.uncheckedAppend(std::pair<Ref<SharedBuffer>, KeyStatus> { *it.first, it.second }); | 
|---|
| 623 |  | 
|---|
| 624 | changedKeys = WTFMove(keyStatusVector); | 
|---|
| 625 | } | 
|---|
| 626 |  | 
|---|
| 627 | dispatchCallback(false, WTFMove(changedKeys), SuccessValue::Succeeded); | 
|---|
| 628 | return; | 
|---|
| 629 | } | 
|---|
| 630 |  | 
|---|
| 631 | // Parse the response using 'license release acknowledgement' formatting, if possible. | 
|---|
| 632 | if (parseLicenseReleaseAcknowledgementFormat(*root)) { | 
|---|
| 633 | // FIXME: Retrieve the key ID information and use it to validate the keys for this sessionId. | 
|---|
| 634 | ClearKeyState::singleton().keys().remove(sessionId); | 
|---|
| 635 | dispatchCallback(true, WTF::nullopt, SuccessValue::Succeeded); | 
|---|
| 636 | return; | 
|---|
| 637 | } | 
|---|
| 638 |  | 
|---|
| 639 | // Bail in case no format was recognized. | 
|---|
| 640 | dispatchCallback(false, WTF::nullopt, SuccessValue::Failed); | 
|---|
| 641 | } | 
|---|
| 642 |  | 
|---|
| 643 | void CDMInstanceSessionClearKey::loadSession(LicenseType, const String& sessionId, const String&, LoadSessionCallback&& callback) | 
|---|
| 644 | { | 
|---|
| 645 | // Use a helper functor that schedules the callback dispatch, avoiding duplicated callOnMainThread() calls. | 
|---|
| 646 | auto dispatchCallback = | 
|---|
| 647 | [this, &callback](Optional<KeyStatusVector>&& existingKeys, SuccessValue success, SessionLoadFailure loadFailure) { | 
|---|
| 648 | callOnMainThread( | 
|---|
| 649 | [weakThis = makeWeakPtr(*this), callback = WTFMove(callback), existingKeys = WTFMove(existingKeys), success, loadFailure]() mutable { | 
|---|
| 650 | if (!weakThis) | 
|---|
| 651 | return; | 
|---|
| 652 |  | 
|---|
| 653 | callback(WTFMove(existingKeys), WTF::nullopt, WTF::nullopt, success, loadFailure); | 
|---|
| 654 | }); | 
|---|
| 655 | }; | 
|---|
| 656 |  | 
|---|
| 657 | // Construct the KeyStatusVector object, representing all the known keys for this session. | 
|---|
| 658 | KeyStatusVector keyStatusVector; | 
|---|
| 659 | { | 
|---|
| 660 | auto& keys = ClearKeyState::singleton().keys(); | 
|---|
| 661 | auto it = keys.find(sessionId); | 
|---|
| 662 | if (it == keys.end()) { | 
|---|
| 663 | dispatchCallback(WTF::nullopt, Failed, SessionLoadFailure::NoSessionData); | 
|---|
| 664 | return; | 
|---|
| 665 | } | 
|---|
| 666 |  | 
|---|
| 667 | auto& keyVector = it->value; | 
|---|
| 668 | keyStatusVector.reserveInitialCapacity(keyVector.size()); | 
|---|
| 669 | for (auto& key : keyVector) | 
|---|
| 670 | keyStatusVector.uncheckedAppend(std::pair<Ref<SharedBuffer>, KeyStatus> { *key.keyIDData, key.status }); | 
|---|
| 671 | } | 
|---|
| 672 |  | 
|---|
| 673 | dispatchCallback(WTFMove(keyStatusVector), Succeeded, SessionLoadFailure::None); | 
|---|
| 674 | } | 
|---|
| 675 |  | 
|---|
| 676 | void CDMInstanceSessionClearKey::closeSession(const String&, CloseSessionCallback&& callback) | 
|---|
| 677 | { | 
|---|
| 678 | callOnMainThread( | 
|---|
| 679 | [weakThis = makeWeakPtr(*this), callback = WTFMove(callback)] () mutable { | 
|---|
| 680 | if (!weakThis) | 
|---|
| 681 | return; | 
|---|
| 682 |  | 
|---|
| 683 | callback(); | 
|---|
| 684 | }); | 
|---|
| 685 | } | 
|---|
| 686 |  | 
|---|
| 687 | void CDMInstanceSessionClearKey::removeSessionData(const String& sessionId, LicenseType, RemoveSessionDataCallback&& callback) | 
|---|
| 688 | { | 
|---|
| 689 | // Use a helper functor that schedules the callback dispatch, avoiding duplicated callOnMainThread() calls. | 
|---|
| 690 | auto dispatchCallback = | 
|---|
| 691 | [this, &callback](KeyStatusVector&& keyStatusVector, Optional<Ref<SharedBuffer>>&& message, SuccessValue success) { | 
|---|
| 692 | callOnMainThread( | 
|---|
| 693 | [weakThis = makeWeakPtr(*this), callback = WTFMove(callback), keyStatusVector = WTFMove(keyStatusVector), message = WTFMove(message), success]() mutable { | 
|---|
| 694 | if (!weakThis) | 
|---|
| 695 | return; | 
|---|
| 696 |  | 
|---|
| 697 | callback(WTFMove(keyStatusVector), WTFMove(message), success); | 
|---|
| 698 | }); | 
|---|
| 699 | }; | 
|---|
| 700 |  | 
|---|
| 701 | // Construct the KeyStatusVector object, representing released keys, and the message in the | 
|---|
| 702 | // 'license release' format. | 
|---|
| 703 | KeyStatusVector keyStatusVector; | 
|---|
| 704 | RefPtr<SharedBuffer> message; | 
|---|
| 705 | { | 
|---|
| 706 | // Retrieve information for the given session ID, bailing if none is found. | 
|---|
| 707 | auto& keys = ClearKeyState::singleton().keys(); | 
|---|
| 708 | auto it = keys.find(sessionId); | 
|---|
| 709 | if (it == keys.end()) { | 
|---|
| 710 | dispatchCallback(KeyStatusVector { }, WTF::nullopt, SuccessValue::Failed); | 
|---|
| 711 | return; | 
|---|
| 712 | } | 
|---|
| 713 |  | 
|---|
| 714 | // Retrieve the Key vector, containing all the keys for this session, and | 
|---|
| 715 | // then remove the key map entry for this session. | 
|---|
| 716 | auto keyVector = WTFMove(it->value); | 
|---|
| 717 | keys.remove(it); | 
|---|
| 718 |  | 
|---|
| 719 | // Construct the KeyStatusVector object, pairing key IDs with the 'released' status. | 
|---|
| 720 | keyStatusVector.reserveInitialCapacity(keyVector.size()); | 
|---|
| 721 | for (auto& key : keyVector) | 
|---|
| 722 | keyStatusVector.uncheckedAppend(std::pair<Ref<SharedBuffer>, KeyStatus> { *key.keyIDData, KeyStatus::Released }); | 
|---|
| 723 |  | 
|---|
| 724 | // Construct JSON that represents the 'license release' format, creating a 'kids' array | 
|---|
| 725 | // of base64URL-encoded key IDs for all keys that were associated with this session. | 
|---|
| 726 | auto rootObject = JSON::Object::create(); | 
|---|
| 727 | { | 
|---|
| 728 | auto array = JSON::Array::create(); | 
|---|
| 729 | for (auto& key : keyVector) { | 
|---|
| 730 | ASSERT(key.keyIDData->size() <= std::numeric_limits<unsigned>::max()); | 
|---|
| 731 | array->pushString(WTF::base64URLEncode(key.keyIDData->data(), static_cast<unsigned>(key.keyIDData->size()))); | 
|---|
| 732 | } | 
|---|
| 733 | rootObject->setArray( "kids", WTFMove(array)); | 
|---|
| 734 | } | 
|---|
| 735 |  | 
|---|
| 736 | // Copy the JSON data into a SharedBuffer object. | 
|---|
| 737 | String messageString = rootObject->toJSONString(); | 
|---|
| 738 | CString messageCString = messageString.utf8(); | 
|---|
| 739 | message = SharedBuffer::create(messageCString.data(), messageCString.length()); | 
|---|
| 740 | } | 
|---|
| 741 |  | 
|---|
| 742 | dispatchCallback(WTFMove(keyStatusVector), Ref<SharedBuffer>(*message), SuccessValue::Succeeded); | 
|---|
| 743 | } | 
|---|
| 744 |  | 
|---|
| 745 | void CDMInstanceSessionClearKey::storeRecordOfKeyUsage(const String&) | 
|---|
| 746 | { | 
|---|
| 747 | } | 
|---|
| 748 |  | 
|---|
| 749 | } // namespace WebCore | 
|---|
| 750 |  | 
|---|
| 751 | #endif // ENABLE(ENCRYPTED_MEDIA) | 
|---|
| 752 |  | 
|---|