- add sources.
[platform/framework/web/crosswalk.git] / src / content / renderer / webcrypto / webcrypto_impl_nss.cc
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/renderer/webcrypto/webcrypto_impl.h"
6
7 #include <cryptohi.h>
8 #include <pk11pub.h>
9 #include <sechash.h>
10
11 #include <vector>
12
13 #include "base/logging.h"
14 #include "crypto/nss_util.h"
15 #include "crypto/scoped_nss_types.h"
16 #include "crypto/secure_util.h"
17 #include "third_party/WebKit/public/platform/WebArrayBuffer.h"
18 #include "third_party/WebKit/public/platform/WebCryptoAlgorithm.h"
19 #include "third_party/WebKit/public/platform/WebCryptoAlgorithmParams.h"
20
21 namespace content {
22
23 namespace {
24
25 class SymKeyHandle : public WebKit::WebCryptoKeyHandle {
26  public:
27   explicit SymKeyHandle(crypto::ScopedPK11SymKey key) : key_(key.Pass()) {}
28
29   PK11SymKey* key() { return key_.get(); }
30
31  private:
32   crypto::ScopedPK11SymKey key_;
33
34   DISALLOW_COPY_AND_ASSIGN(SymKeyHandle);
35 };
36
37 class PublicKeyHandle : public WebKit::WebCryptoKeyHandle {
38  public:
39   explicit PublicKeyHandle(crypto::ScopedSECKEYPublicKey key)
40       : key_(key.Pass()) {}
41
42   SECKEYPublicKey* key() { return key_.get(); }
43
44  private:
45   crypto::ScopedSECKEYPublicKey key_;
46
47   DISALLOW_COPY_AND_ASSIGN(PublicKeyHandle);
48 };
49
50 class PrivateKeyHandle : public WebKit::WebCryptoKeyHandle {
51  public:
52   explicit PrivateKeyHandle(crypto::ScopedSECKEYPrivateKey key)
53       : key_(key.Pass()) {}
54
55   SECKEYPrivateKey* key() { return key_.get(); }
56
57  private:
58   crypto::ScopedSECKEYPrivateKey key_;
59
60   DISALLOW_COPY_AND_ASSIGN(PrivateKeyHandle);
61 };
62
63 HASH_HashType WebCryptoAlgorithmToNSSHashType(
64     const WebKit::WebCryptoAlgorithm& algorithm) {
65   switch (algorithm.id()) {
66     case WebKit::WebCryptoAlgorithmIdSha1:
67       return HASH_AlgSHA1;
68     case WebKit::WebCryptoAlgorithmIdSha224:
69       return HASH_AlgSHA224;
70     case WebKit::WebCryptoAlgorithmIdSha256:
71       return HASH_AlgSHA256;
72     case WebKit::WebCryptoAlgorithmIdSha384:
73       return HASH_AlgSHA384;
74     case WebKit::WebCryptoAlgorithmIdSha512:
75       return HASH_AlgSHA512;
76     default:
77       // Not a digest algorithm.
78       return HASH_AlgNULL;
79   }
80 }
81
82 CK_MECHANISM_TYPE WebCryptoAlgorithmToHMACMechanism(
83     const WebKit::WebCryptoAlgorithm& algorithm) {
84   switch (algorithm.id()) {
85     case WebKit::WebCryptoAlgorithmIdSha1:
86       return CKM_SHA_1_HMAC;
87     case WebKit::WebCryptoAlgorithmIdSha256:
88       return CKM_SHA256_HMAC;
89     default:
90       // Not a supported algorithm.
91       return CKM_INVALID_MECHANISM;
92   }
93 }
94
95 bool AesCbcEncryptDecrypt(
96     CK_ATTRIBUTE_TYPE operation,
97     const WebKit::WebCryptoAlgorithm& algorithm,
98     const WebKit::WebCryptoKey& key,
99     const unsigned char* data,
100     unsigned data_size,
101     WebKit::WebArrayBuffer* buffer) {
102   DCHECK_EQ(WebKit::WebCryptoAlgorithmIdAesCbc, algorithm.id());
103   DCHECK_EQ(algorithm.id(), key.algorithm().id());
104   DCHECK_EQ(WebKit::WebCryptoKeyTypeSecret, key.type());
105   DCHECK(operation == CKA_ENCRYPT || operation == CKA_DECRYPT);
106
107   SymKeyHandle* sym_key = reinterpret_cast<SymKeyHandle*>(key.handle());
108
109   const WebKit::WebCryptoAesCbcParams* params = algorithm.aesCbcParams();
110   if (params->iv().size() != AES_BLOCK_SIZE)
111     return false;
112
113   SECItem iv_item;
114   iv_item.type = siBuffer;
115   iv_item.data = const_cast<unsigned char*>(params->iv().data());
116   iv_item.len = params->iv().size();
117
118   crypto::ScopedSECItem param(PK11_ParamFromIV(CKM_AES_CBC_PAD, &iv_item));
119   if (!param)
120     return false;
121
122   crypto::ScopedPK11Context context(PK11_CreateContextBySymKey(
123       CKM_AES_CBC_PAD, operation, sym_key->key(), param.get()));
124
125   if (!context.get())
126     return false;
127
128   // Oddly PK11_CipherOp takes input and output lengths as "int" rather than
129   // "unsigned". Do some checks now to avoid integer overflowing.
130   if (data_size >= INT_MAX - AES_BLOCK_SIZE) {
131     // TODO(eroman): Handle this by chunking the input fed into NSS. Right now
132     // it doesn't make much difference since the one-shot API would end up
133     // blowing out the memory and crashing anyway. However a newer version of
134     // the spec allows for a sequence<CryptoData> so this will be relevant.
135     return false;
136   }
137
138   // PK11_CipherOp does an invalid memory access when given empty decryption
139   // input, or input which is not a multiple of the block size. See also
140   // https://bugzilla.mozilla.com/show_bug.cgi?id=921687.
141   if (operation == CKA_DECRYPT &&
142       (data_size == 0 || (data_size % AES_BLOCK_SIZE != 0))) {
143     return false;
144   }
145
146   // TODO(eroman): Refine the output buffer size. It can be computed exactly for
147   //               encryption, and can be smaller for decryption.
148   unsigned output_max_len = data_size + AES_BLOCK_SIZE;
149   CHECK_GT(output_max_len, data_size);
150
151   *buffer = WebKit::WebArrayBuffer::create(output_max_len, 1);
152
153   unsigned char* buffer_data = reinterpret_cast<unsigned char*>(buffer->data());
154
155   int output_len;
156   if (SECSuccess != PK11_CipherOp(context.get(),
157                                   buffer_data,
158                                   &output_len,
159                                   buffer->byteLength(),
160                                   data,
161                                   data_size)) {
162     return false;
163   }
164
165   unsigned int final_output_chunk_len;
166   if (SECSuccess != PK11_DigestFinal(context.get(),
167                                      buffer_data + output_len,
168                                      &final_output_chunk_len,
169                                      output_max_len - output_len)) {
170     return false;
171   }
172
173   WebCryptoImpl::ShrinkBuffer(buffer, final_output_chunk_len + output_len);
174   return true;
175 }
176
177 CK_MECHANISM_TYPE HmacAlgorithmToGenMechanism(
178     const WebKit::WebCryptoAlgorithm& algorithm) {
179   DCHECK_EQ(algorithm.id(), WebKit::WebCryptoAlgorithmIdHmac);
180   const WebKit::WebCryptoHmacKeyParams* params = algorithm.hmacKeyParams();
181   DCHECK(params);
182   switch (params->hash().id()) {
183     case WebKit::WebCryptoAlgorithmIdSha1:
184       return CKM_SHA_1_HMAC;
185     case WebKit::WebCryptoAlgorithmIdSha256:
186       return CKM_SHA256_HMAC;
187     default:
188       return CKM_INVALID_MECHANISM;
189   }
190 }
191
192 CK_MECHANISM_TYPE WebCryptoAlgorithmToGenMechanism(
193     const WebKit::WebCryptoAlgorithm& algorithm) {
194   switch (algorithm.id()) {
195     case WebKit::WebCryptoAlgorithmIdAesCbc:
196       return CKM_AES_KEY_GEN;
197     case WebKit::WebCryptoAlgorithmIdHmac:
198       return HmacAlgorithmToGenMechanism(algorithm);
199     default:
200       return CKM_INVALID_MECHANISM;
201   }
202 }
203
204 unsigned int WebCryptoHmacAlgorithmToBlockSize(
205     const WebKit::WebCryptoAlgorithm& algorithm) {
206   DCHECK_EQ(algorithm.id(), WebKit::WebCryptoAlgorithmIdHmac);
207   const WebKit::WebCryptoHmacKeyParams* params = algorithm.hmacKeyParams();
208   DCHECK(params);
209   switch (params->hash().id()) {
210     case WebKit::WebCryptoAlgorithmIdSha1:
211       return 512;
212     case WebKit::WebCryptoAlgorithmIdSha256:
213       return 512;
214     default:
215       return 0;
216   }
217 }
218
219 // Converts a (big-endian) WebCrypto BigInteger, with or without leading zeros,
220 // to unsigned long.
221 bool BigIntegerToLong(const uint8* data,
222                       unsigned data_size,
223                       unsigned long* result) {
224   // TODO(padolph): Is it correct to say that empty data is an error, or does it
225   // mean value 0? See https://www.w3.org/Bugs/Public/show_bug.cgi?id=23655
226   if (data_size == 0)
227     return false;
228
229   *result = 0;
230   for (size_t i = 0; i < data_size; ++i) {
231     size_t reverse_i = data_size - i - 1;
232
233     if (reverse_i >= sizeof(unsigned long) && data[i])
234       return false;  // Too large for a long.
235
236     *result |= data[i] << 8 * reverse_i;
237   }
238   return true;
239 }
240
241 }  // namespace
242
243 void WebCryptoImpl::Init() {
244   crypto::EnsureNSSInit();
245 }
246
247 bool WebCryptoImpl::EncryptInternal(
248     const WebKit::WebCryptoAlgorithm& algorithm,
249     const WebKit::WebCryptoKey& key,
250     const unsigned char* data,
251     unsigned data_size,
252     WebKit::WebArrayBuffer* buffer) {
253   if (algorithm.id() == WebKit::WebCryptoAlgorithmIdAesCbc) {
254     return AesCbcEncryptDecrypt(
255         CKA_ENCRYPT, algorithm, key, data, data_size, buffer);
256   }
257
258   return false;
259 }
260
261 bool WebCryptoImpl::DecryptInternal(
262     const WebKit::WebCryptoAlgorithm& algorithm,
263     const WebKit::WebCryptoKey& key,
264     const unsigned char* data,
265     unsigned data_size,
266     WebKit::WebArrayBuffer* buffer) {
267   if (algorithm.id() == WebKit::WebCryptoAlgorithmIdAesCbc) {
268     return AesCbcEncryptDecrypt(
269         CKA_DECRYPT, algorithm, key, data, data_size, buffer);
270   }
271
272   return false;
273 }
274
275 bool WebCryptoImpl::DigestInternal(
276     const WebKit::WebCryptoAlgorithm& algorithm,
277     const unsigned char* data,
278     unsigned data_size,
279     WebKit::WebArrayBuffer* buffer) {
280   HASH_HashType hash_type = WebCryptoAlgorithmToNSSHashType(algorithm);
281   if (hash_type == HASH_AlgNULL) {
282     return false;
283   }
284
285   HASHContext* context = HASH_Create(hash_type);
286   if (!context) {
287     return false;
288   }
289
290   HASH_Begin(context);
291
292   HASH_Update(context, data, data_size);
293
294   unsigned hash_result_length = HASH_ResultLenContext(context);
295   DCHECK_LE(hash_result_length, static_cast<size_t>(HASH_LENGTH_MAX));
296
297   *buffer = WebKit::WebArrayBuffer::create(hash_result_length, 1);
298
299   unsigned char* digest = reinterpret_cast<unsigned char*>(buffer->data());
300
301   unsigned result_length = 0;
302   HASH_End(context, digest, &result_length, hash_result_length);
303
304   HASH_Destroy(context);
305
306   return result_length == hash_result_length;
307 }
308
309 bool WebCryptoImpl::GenerateKeyInternal(
310     const WebKit::WebCryptoAlgorithm& algorithm,
311     bool extractable,
312     WebKit::WebCryptoKeyUsageMask usage_mask,
313     WebKit::WebCryptoKey* key) {
314
315   CK_MECHANISM_TYPE mech = WebCryptoAlgorithmToGenMechanism(algorithm);
316   unsigned int keylen_bytes = 0;
317   WebKit::WebCryptoKeyType key_type = WebKit::WebCryptoKeyTypeSecret;
318
319   if (mech == CKM_INVALID_MECHANISM) {
320     return false;
321   }
322
323   switch (algorithm.id()) {
324     case WebKit::WebCryptoAlgorithmIdAesCbc: {
325       const WebKit::WebCryptoAesKeyGenParams* params =
326           algorithm.aesKeyGenParams();
327       DCHECK(params);
328       keylen_bytes = params->length() / 8;
329       if (params->length() % 8)
330         return false;
331       key_type = WebKit::WebCryptoKeyTypeSecret;
332       break;
333     }
334     case WebKit::WebCryptoAlgorithmIdHmac: {
335       const WebKit::WebCryptoHmacKeyParams* params = algorithm.hmacKeyParams();
336       DCHECK(params);
337       if (!params->getLength(keylen_bytes)) {
338         keylen_bytes = WebCryptoHmacAlgorithmToBlockSize(algorithm) / 8;
339       }
340
341       key_type = WebKit::WebCryptoKeyTypeSecret;
342       break;
343     }
344
345     default: {
346       return false;
347     }
348   }
349
350   if (keylen_bytes == 0) {
351     return false;
352   }
353
354   crypto::ScopedPK11Slot slot(PK11_GetInternalKeySlot());
355   if (!slot) {
356     return false;
357   }
358
359   crypto::ScopedPK11SymKey pk11_key(
360       PK11_KeyGen(slot.get(), mech, NULL, keylen_bytes, NULL));
361
362   if (!pk11_key) {
363     return false;
364   }
365
366   *key = WebKit::WebCryptoKey::create(
367       new SymKeyHandle(pk11_key.Pass()),
368       key_type, extractable, algorithm, usage_mask);
369   return true;
370 }
371
372 bool WebCryptoImpl::GenerateKeyPairInternal(
373     const WebKit::WebCryptoAlgorithm& algorithm,
374     bool extractable,
375     WebKit::WebCryptoKeyUsageMask usage_mask,
376     WebKit::WebCryptoKey* public_key,
377     WebKit::WebCryptoKey* private_key) {
378
379   // TODO(padolph): Handle other asymmetric algorithm key generation.
380   switch (algorithm.id()) {
381     case WebKit::WebCryptoAlgorithmIdRsaEsPkcs1v1_5:
382     case WebKit::WebCryptoAlgorithmIdRsaOaep:
383     case WebKit::WebCryptoAlgorithmIdRsaSsaPkcs1v1_5: {
384       const WebKit::WebCryptoRsaKeyGenParams* const params =
385           algorithm.rsaKeyGenParams();
386       DCHECK(params);
387
388       crypto::ScopedPK11Slot slot(PK11_GetInternalKeySlot());
389       unsigned long public_exponent;
390       if (!slot || !params->modulusLength() ||
391           !BigIntegerToLong(params->publicExponent().data(),
392                             params->publicExponent().size(),
393                             &public_exponent) ||
394           !public_exponent) {
395         return false;
396       }
397
398       PK11RSAGenParams rsa_gen_params;
399       rsa_gen_params.keySizeInBits = params->modulusLength();
400       rsa_gen_params.pe = public_exponent;
401
402       // Flags are verified at the Blink layer; here the flags are set to all
403       // possible operations for the given key type.
404       CK_FLAGS operation_flags;
405       switch (algorithm.id()) {
406         case WebKit::WebCryptoAlgorithmIdRsaEsPkcs1v1_5:
407         case WebKit::WebCryptoAlgorithmIdRsaOaep:
408           operation_flags = CKF_ENCRYPT | CKF_DECRYPT | CKF_WRAP | CKF_UNWRAP;
409           break;
410         case WebKit::WebCryptoAlgorithmIdRsaSsaPkcs1v1_5:
411           operation_flags = CKF_SIGN | CKF_VERIFY;
412           break;
413         default:
414           NOTREACHED();
415           return false;
416       }
417       const CK_FLAGS operation_flags_mask = CKF_ENCRYPT | CKF_DECRYPT |
418                                             CKF_SIGN | CKF_VERIFY | CKF_WRAP |
419                                             CKF_UNWRAP;
420       const PK11AttrFlags attribute_flags = 0;  // Default all PK11_ATTR_ flags.
421
422       // Note: NSS does not generate an sec_public_key if the call below fails,
423       // so there is no danger of a leaked sec_public_key.
424       SECKEYPublicKey* sec_public_key;
425       crypto::ScopedSECKEYPrivateKey scoped_sec_private_key(
426           PK11_GenerateKeyPairWithOpFlags(slot.get(),
427                                           CKM_RSA_PKCS_KEY_PAIR_GEN,
428                                           &rsa_gen_params,
429                                           &sec_public_key,
430                                           attribute_flags,
431                                           operation_flags,
432                                           operation_flags_mask,
433                                           NULL));
434       if (!private_key) {
435         return false;
436       }
437
438       // One extractable input parameter is provided, and the Web Crypto API
439       // spec at this time says it applies to both members of the key pair.
440       // This is probably not correct: it makes more operational sense to have
441       // extractable apply only to the private key and make the public key
442       // always extractable. For now implement what the spec says and track the
443       // spec bug here: https://www.w3.org/Bugs/Public/show_bug.cgi?id=23695
444       *public_key = WebKit::WebCryptoKey::create(
445           new PublicKeyHandle(crypto::ScopedSECKEYPublicKey(sec_public_key)),
446           WebKit::WebCryptoKeyTypePublic,
447           extractable,   // probably should be 'true' always
448           algorithm,
449           usage_mask);
450       *private_key = WebKit::WebCryptoKey::create(
451           new PrivateKeyHandle(scoped_sec_private_key.Pass()),
452           WebKit::WebCryptoKeyTypePrivate,
453           extractable,
454           algorithm,
455           usage_mask);
456
457       return true;
458     }
459     default:
460       return false;
461   }
462 }
463
464 bool WebCryptoImpl::ImportKeyInternal(
465     WebKit::WebCryptoKeyFormat format,
466     const unsigned char* key_data,
467     unsigned key_data_size,
468     const WebKit::WebCryptoAlgorithm& algorithm_or_null,
469     bool extractable,
470     WebKit::WebCryptoKeyUsageMask usage_mask,
471     WebKit::WebCryptoKey* key) {
472   // TODO(eroman): Currently expects algorithm to always be specified, as it is
473   //               required for raw format.
474   if (algorithm_or_null.isNull())
475     return false;
476   const WebKit::WebCryptoAlgorithm& algorithm = algorithm_or_null;
477
478   WebKit::WebCryptoKeyType type;
479   switch (algorithm.id()) {
480     case WebKit::WebCryptoAlgorithmIdHmac:
481     case WebKit::WebCryptoAlgorithmIdAesCbc:
482       type = WebKit::WebCryptoKeyTypeSecret;
483       break;
484     // TODO(bryaneyler): Support more key types.
485     default:
486       return false;
487   }
488
489   // TODO(bryaneyler): Need to split handling for symmetric and asymmetric keys.
490   // Currently only supporting symmetric.
491   CK_MECHANISM_TYPE mechanism = CKM_INVALID_MECHANISM;
492   // Flags are verified at the Blink layer; here the flags are set to all
493   // possible operations for this key type.
494   CK_FLAGS flags = 0;
495
496   switch(algorithm.id()) {
497     case WebKit::WebCryptoAlgorithmIdHmac: {
498       const WebKit::WebCryptoHmacParams* params = algorithm.hmacParams();
499       if (!params) {
500         return false;
501       }
502
503       mechanism = WebCryptoAlgorithmToHMACMechanism(params->hash());
504       if (mechanism == CKM_INVALID_MECHANISM) {
505         return false;
506       }
507
508       flags |= CKF_SIGN | CKF_VERIFY;
509
510       break;
511     }
512     case WebKit::WebCryptoAlgorithmIdAesCbc: {
513       mechanism = CKM_AES_CBC;
514       flags |= CKF_ENCRYPT | CKF_DECRYPT;
515       break;
516     }
517     default:
518       return false;
519   }
520
521   DCHECK_NE(CKM_INVALID_MECHANISM, mechanism);
522   DCHECK_NE(0ul, flags);
523
524   SECItem key_item = { siBuffer, NULL, 0 };
525
526   switch (format) {
527     case WebKit::WebCryptoKeyFormatRaw:
528       key_item.data = const_cast<unsigned char*>(key_data);
529       key_item.len = key_data_size;
530       break;
531     // TODO(bryaneyler): Handle additional formats.
532     default:
533       return false;
534   }
535
536   crypto::ScopedPK11SymKey pk11_sym_key(
537       PK11_ImportSymKeyWithFlags(PK11_GetInternalSlot(),
538                                  mechanism,
539                                  PK11_OriginUnwrap,
540                                  CKA_FLAGS_ONLY,
541                                  &key_item,
542                                  flags,
543                                  false,
544                                  NULL));
545   if (!pk11_sym_key.get()) {
546     return false;
547   }
548
549   *key = WebKit::WebCryptoKey::create(new SymKeyHandle(pk11_sym_key.Pass()),
550                                       type, extractable, algorithm, usage_mask);
551   return true;
552 }
553
554 bool WebCryptoImpl::SignInternal(
555     const WebKit::WebCryptoAlgorithm& algorithm,
556     const WebKit::WebCryptoKey& key,
557     const unsigned char* data,
558     unsigned data_size,
559     WebKit::WebArrayBuffer* buffer) {
560   WebKit::WebArrayBuffer result;
561
562   switch (algorithm.id()) {
563     case WebKit::WebCryptoAlgorithmIdHmac: {
564       const WebKit::WebCryptoHmacParams* params = algorithm.hmacParams();
565       if (!params) {
566         return false;
567       }
568
569       SymKeyHandle* sym_key = reinterpret_cast<SymKeyHandle*>(key.handle());
570
571       DCHECK_EQ(PK11_GetMechanism(sym_key->key()),
572                 WebCryptoAlgorithmToHMACMechanism(params->hash()));
573       DCHECK_NE(0, key.usages() & WebKit::WebCryptoKeyUsageSign);
574
575       SECItem param_item = { siBuffer, NULL, 0 };
576       SECItem data_item = {
577         siBuffer,
578         const_cast<unsigned char*>(data),
579         data_size
580       };
581       // First call is to figure out the length.
582       SECItem signature_item = { siBuffer, NULL, 0 };
583
584       if (PK11_SignWithSymKey(sym_key->key(),
585                               PK11_GetMechanism(sym_key->key()),
586                               &param_item,
587                               &signature_item,
588                               &data_item) != SECSuccess) {
589         NOTREACHED();
590         return false;
591       }
592
593       DCHECK_NE(0u, signature_item.len);
594
595       result = WebKit::WebArrayBuffer::create(signature_item.len, 1);
596       signature_item.data = reinterpret_cast<unsigned char*>(result.data());
597
598       if (PK11_SignWithSymKey(sym_key->key(),
599                               PK11_GetMechanism(sym_key->key()),
600                               &param_item,
601                               &signature_item,
602                               &data_item) != SECSuccess) {
603         NOTREACHED();
604         return false;
605       }
606
607       DCHECK_EQ(result.byteLength(), signature_item.len);
608
609       break;
610     }
611     default:
612       return false;
613   }
614
615   *buffer = result;
616   return true;
617 }
618
619 bool WebCryptoImpl::VerifySignatureInternal(
620     const WebKit::WebCryptoAlgorithm& algorithm,
621     const WebKit::WebCryptoKey& key,
622     const unsigned char* signature,
623     unsigned signature_size,
624     const unsigned char* data,
625     unsigned data_size,
626     bool* signature_match) {
627   switch (algorithm.id()) {
628     case WebKit::WebCryptoAlgorithmIdHmac: {
629       WebKit::WebArrayBuffer result;
630       if (!SignInternal(algorithm, key, data, data_size, &result)) {
631         return false;
632       }
633
634       // Handling of truncated signatures is underspecified in the WebCrypto
635       // spec, so here we fail verification if a truncated signature is being
636       // verified.
637       // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=23097
638       *signature_match =
639           result.byteLength() == signature_size &&
640           crypto::SecureMemEqual(result.data(), signature, signature_size);
641
642       break;
643     }
644     default:
645       return false;
646   }
647
648   return true;
649 }
650
651 }  // namespace content