Index: content/renderer/webcrypto/webcrypto_impl.cc |
diff --git a/content/renderer/webcrypto/webcrypto_impl.cc b/content/renderer/webcrypto/webcrypto_impl.cc |
index 6eeb3daf5a0cb07de9dc19128ed3e2913338054b..d4562f8fcc67ee8f46243bc50eb3bf9b6537b95b 100644 |
--- a/content/renderer/webcrypto/webcrypto_impl.cc |
+++ b/content/renderer/webcrypto/webcrypto_impl.cc |
@@ -4,15 +4,242 @@ |
#include "content/renderer/webcrypto/webcrypto_impl.h" |
+#include <algorithm> |
+#include <map> |
+#include "base/base64.h" |
+#include "base/json/json_reader.h" |
+#include "base/logging.h" |
#include "base/memory/scoped_ptr.h" |
+#include "base/strings/string_piece.h" |
+#include "base/values.h" |
#include "third_party/WebKit/public/platform/WebArrayBuffer.h" |
#include "third_party/WebKit/public/platform/WebCryptoAlgorithm.h" |
+#include "third_party/WebKit/public/platform/WebCryptoAlgorithmParams.h" |
#include "third_party/WebKit/public/platform/WebCryptoKey.h" |
namespace content { |
+namespace { |
+ |
+// TODO(padolph) Similar Create*Algorithm() methods are in |
+// webcrypto_impl_unittest.cc. Find a common place to put these. |
+ |
+WebKit::WebCryptoAlgorithm CreateAlgorithm(WebKit::WebCryptoAlgorithmId id) { |
+ return WebKit::WebCryptoAlgorithm::adoptParamsAndCreate(id, NULL); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateAlgorithmWithInnerHash( |
+ WebKit::WebCryptoAlgorithmId algorithm_id, |
+ unsigned short hash_key_length) { |
+ WebKit::WebCryptoAlgorithmId hashId; |
+ switch (hash_key_length) { |
+ case 160: |
+ hashId = WebKit::WebCryptoAlgorithmIdSha1; |
+ break; |
+ case 224: |
+ hashId = WebKit::WebCryptoAlgorithmIdSha224; |
+ break; |
+ case 256: |
+ hashId = WebKit::WebCryptoAlgorithmIdSha256; |
+ break; |
+ case 384: |
+ hashId = WebKit::WebCryptoAlgorithmIdSha384; |
+ break; |
+ case 512: |
+ hashId = WebKit::WebCryptoAlgorithmIdSha384; |
+ break; |
+ } |
+ return WebKit::WebCryptoAlgorithm::adoptParamsAndCreate( |
+ algorithm_id, |
+ new WebKit::WebCryptoHmacParams(CreateAlgorithm(hashId))); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateHmacAlgorithm(unsigned short hash_key_length) { |
+ return CreateAlgorithmWithInnerHash( |
+ WebKit::WebCryptoAlgorithmIdHmac, |
+ hash_key_length); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateRsaSsaAlgorithm( |
+ unsigned short hash_key_length) { |
+ return CreateAlgorithmWithInnerHash( |
+ WebKit::WebCryptoAlgorithmIdRsaSsaPkcs1v1_5, |
+ hash_key_length); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateRsaOaepAlgorithm( |
+ unsigned short hash_key_length) { |
+ return CreateAlgorithmWithInnerHash( |
+ WebKit::WebCryptoAlgorithmIdRsaOaep, |
+ hash_key_length); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateAesAlgorithm( |
+ WebKit::WebCryptoAlgorithmId aes_alg_id, |
+ unsigned short length) { |
+ return WebKit::WebCryptoAlgorithm::adoptParamsAndCreate( |
+ aes_alg_id, |
+ new WebKit::WebCryptoAesKeyGenParams(length)); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateAesCbcAlgorithm(unsigned short length) { |
+ return CreateAesAlgorithm(WebKit::WebCryptoAlgorithmIdAesCbc, length); |
+} |
+ |
+WebKit::WebCryptoAlgorithm CreateAesGcmAlgorithm(unsigned short length) { |
+ return CreateAesAlgorithm(WebKit::WebCryptoAlgorithmIdAesGcm, length); |
+} |
+ |
+bool Base64DecodeUrlSafe(const std::string& input, std::string* output) { |
+ std::string base64EncodedText(input); |
+ std::replace(base64EncodedText.begin(), base64EncodedText.end(), '-', '+'); |
+ std::replace(base64EncodedText.begin(), base64EncodedText.end(), '_', '/'); |
+ base64EncodedText.append((4 - base64EncodedText.size() % 4) % 4, '='); |
+ return base::Base64Decode(base64EncodedText, output); |
+} |
+ |
+// Identifiers for all JWK "alg" (algorithm) values handled by this code. The |
+// "enum trick" is used to force int type, so a user type is not required in |
+// web_crypto_impl.h |
+enum { |
+ kJwkAlgorithmHs256, |
+ kJwkAlgorithmHs384, |
+ kJwkAlgorithmHs512, |
+ kJwkAlgorithmRs256, |
+ kJwkAlgorithmRs384, |
+ kJwkAlgorithmRs512, |
+ kJwkAlgorithmRsa1_5, |
+ kJwkAlgorithmRsaOaep, |
+ kJwkAlgorithmA128Kw, |
+ kJwkAlgorithmA256Kw, |
+ kJwkAlgorithmA128Gcm, |
+ kJwkAlgorithmA256Gcm, |
+ kJwkAlgorithmA128Cbc, |
+ kJwkAlgorithmA256Cbc, |
+ kJwkAlgorithmA384Cbc, |
+ kJwkAlgorithmA512Cbc |
+}; |
+ |
+WebKit::WebCryptoAlgorithmId JwkAlgIdToWebCryptoAlgId(int jwk_algorithm_id) { |
+ switch (jwk_algorithm_id) { |
+ case kJwkAlgorithmHs256: |
+ case kJwkAlgorithmHs384: |
+ case kJwkAlgorithmHs512: |
+ return WebKit::WebCryptoAlgorithmIdHmac; |
+ case kJwkAlgorithmRs256: |
+ case kJwkAlgorithmRs384: |
+ case kJwkAlgorithmRs512: |
+ return WebKit::WebCryptoAlgorithmIdRsaSsaPkcs1v1_5; |
+ case kJwkAlgorithmRsa1_5: |
+ return WebKit::WebCryptoAlgorithmIdRsaEsPkcs1v1_5; |
+ case kJwkAlgorithmRsaOaep: |
+ return WebKit::WebCryptoAlgorithmIdRsaOaep; |
+ case kJwkAlgorithmA128Kw: |
+ case kJwkAlgorithmA256Kw: |
+ // TODO(padolph) Support AES keywrap algorithm, required for JWK but not |
+ // present in the Web Crypto spec. |
+ return WebKit::WebCryptoAlgorithmIdNone; |
+ case kJwkAlgorithmA128Gcm: |
+ case kJwkAlgorithmA256Gcm: |
+ return WebKit::WebCryptoAlgorithmIdAesGcm; |
+ case kJwkAlgorithmA128Cbc: |
+ case kJwkAlgorithmA256Cbc: |
+ case kJwkAlgorithmA384Cbc: |
+ case kJwkAlgorithmA512Cbc: |
+ return WebKit::WebCryptoAlgorithmIdAesCbc; |
+ default: |
+ DCHECK(false); |
+ return WebKit::WebCryptoAlgorithmIdNone; |
+ } |
+} |
+ |
+unsigned short JwkAlgIdToKeyLengthBits(int jwk_algorithm_id) { |
+ switch (jwk_algorithm_id) { |
+ case kJwkAlgorithmA128Kw: |
+ case kJwkAlgorithmA128Gcm: |
+ case kJwkAlgorithmA128Cbc: |
+ return 128; |
+ case kJwkAlgorithmHs256: |
+ case kJwkAlgorithmA256Kw: |
+ case kJwkAlgorithmRs256: |
+ case kJwkAlgorithmA256Gcm: |
+ case kJwkAlgorithmA256Cbc: |
+ return 256; |
+ case kJwkAlgorithmHs384: |
+ case kJwkAlgorithmRs384: |
+ case kJwkAlgorithmA384Cbc: |
+ return 384; |
+ case kJwkAlgorithmHs512: |
+ case kJwkAlgorithmRs512: |
+ case kJwkAlgorithmA512Cbc: |
+ return 512; |
+ default: |
+ return 0; |
+ } |
+} |
+ |
+typedef std::map<std::string, int> StringIntMap; |
+ |
+// Syntactic sugar to make runtime initialization of a std::map more palatable, |
+// similar to what you can do in C++11 or with Boost.Assign. |
+// Example: |
+// StringIntMap my_map; |
+// // instead of this |
+// my_map.insert(std::make_pair("a", 1); |
+// my_map.insert(std::make_pair("b", 2); |
+// my_map.insert(std::make_pair("c", 3); |
+// my_map.insert(std::make_pair("d", 4); |
+// // do this |
+// map_fill<StringIntMap> |
+// ("a", 1) |
+// ("b", 2) |
+// ("c", 3) |
+// ("d", 4) |
+// .to(my_map); |
Ryan Sleevi
2013/10/04 23:19:11
Why is this syntactic sugar needed, when
my_map["
padolph
2013/10/05 02:57:42
Done.
|
+template <typename MapType> |
+class FillMap { |
+ public: |
+ typedef typename MapType::key_type KeyType; |
+ typedef typename MapType::mapped_type MappedType; |
+ FillMap(const KeyType& key, const MappedType& val) { operator()(key, val); } |
+ FillMap& operator()(const KeyType& key, const MappedType& val) { |
+ map_.insert(std::make_pair(key, val)); |
+ return *this; |
+ } |
+ void to(MapType& map) { map.swap(map_); } |
+ private: |
+ MapType map_; |
+}; |
+ |
+void InitJwkAlgorithmMap(StringIntMap& jwk_algorithm_map) |
+{ |
+ // Note: A*CBC are not yet present in the JOSE JWA spec |
+ // http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-16 |
+ FillMap<StringIntMap> |
+ ("HS256" , kJwkAlgorithmHs256 ) |
+ ("HS384" , kJwkAlgorithmHs384 ) |
+ ("HS512" , kJwkAlgorithmHs512 ) |
+ ("RS256" , kJwkAlgorithmRs256 ) |
+ ("RS384" , kJwkAlgorithmRs384 ) |
+ ("RS512" , kJwkAlgorithmRs512 ) |
+ ("RSA1_5" , kJwkAlgorithmRsa1_5 ) |
+ ("RSA-OAEP", kJwkAlgorithmRsaOaep) |
+ ("A128KW" , kJwkAlgorithmA128Kw ) |
+ ("A256KW" , kJwkAlgorithmA256Kw ) |
+ ("A128GCM" , kJwkAlgorithmA128Gcm) |
+ ("A256GCM" , kJwkAlgorithmA256Gcm) |
+ ("A128CBC" , kJwkAlgorithmA128Cbc) |
+ ("A256CBC" , kJwkAlgorithmA256Cbc) |
+ ("A384CBC" , kJwkAlgorithmA384Cbc) |
+ ("A512CBC" , kJwkAlgorithmA512Cbc) |
+ .to(jwk_algorithm_map); |
+} |
+ |
+} // namespace |
+ |
WebCryptoImpl::WebCryptoImpl() { |
Init(); |
+ InitJwkAlgorithmMap(jwk_algorithm_map_); |
} |
void WebCryptoImpl::encrypt( |
@@ -66,21 +293,32 @@ void WebCryptoImpl::importKey( |
WebKit::WebCryptoResult result) { |
WebKit::WebCryptoKeyType type; |
scoped_ptr<WebKit::WebCryptoKeyHandle> handle; |
+ WebKit::WebCryptoAlgorithm modified_algorithm = algorithm; |
- if (!ImportKeyInternal(format, |
- key_data, |
- key_data_size, |
- algorithm, |
- usage_mask, |
- &handle, |
- &type)) { |
- result.completeWithError(); |
- return; |
+ if (format == WebKit::WebCryptoKeyFormatJwk) { |
+ if (!ImportKeyJwk(key_data, |
+ key_data_size, |
+ &handle, |
+ &type, |
+ &extractable, |
+ &modified_algorithm, |
+ &usage_mask)) { |
+ result.completeWithError(); |
+ } |
+ } else { |
+ if (!ImportKeyInternal(format, |
+ key_data, |
+ key_data_size, |
+ modified_algorithm, |
+ usage_mask, |
+ &handle, |
+ &type)) { |
+ result.completeWithError(); |
+ } |
} |
- WebKit::WebCryptoKey key( |
- WebKit::WebCryptoKey::create( |
- handle.release(), type, extractable, algorithm, usage_mask)); |
+ WebKit::WebCryptoKey key(WebKit::WebCryptoKey::create( |
+ handle.release(), type, extractable, modified_algorithm, usage_mask)); |
result.completeWithKey(key); |
} |
@@ -121,4 +359,214 @@ void WebCryptoImpl::verifySignature( |
} |
} |
+bool WebCryptoImpl::ImportKeyJwk( |
+ const unsigned char* key_data, |
+ unsigned key_data_size, |
+ scoped_ptr<WebKit::WebCryptoKeyHandle>* handle, |
+ WebKit::WebCryptoKeyType* type, |
+ bool* extractable, |
+ WebKit::WebCryptoAlgorithm* algorithm, |
+ WebKit::WebCryptoKeyUsageMask* usage_mask) { |
+ |
+ // JSON Web Key Format (JWK) |
+ // http://self-issued.info/docs/draft-ietf-jose-json-web-key.html (JOSE) |
Ryan Sleevi
2013/10/04 23:19:11
Let's refer to a canonical draft URL at the IETF,
padolph
2013/10/05 02:57:42
Done.
|
+ // TODO(padolph) Not all possible values are handled by this code right now |
+ // |
+ // A JWK is a simple JSON dictionary with the following entries |
+ // - "kty" (Key Type) Parameter, REQUIRED |
+ // - <kty-specific parameters, see below>, REQUIRED |
+ // - "use" (Key Use) Parameter, OPTIONAL |
+ // - "alg" (Algorithm) Parameter, OPTIONAL |
+ // - "extractable" (Key Exportability), OPTIONAL [NOTE: not yet part of JOSE] |
+ // (all other entries are ignored) |
+ // |
+ // Input key_data contains the JWK. To build a Web Crypto Key, the JWK values |
+ // are parsed out and used as follows: |
+ // Web Crypto Key type <-- (deduced) |
+ // Web Crypto Key extractable <-- extractable |
+ // Web Crypto Key algorithm <-- alg |
+ // Web Crypto Key keyUsage <-- usage |
+ // Web Crypto Key keying material <-- kty-specific parameters |
+ // |
+ // Values for each entry are case-sensitive and defined in |
+ // http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-16. |
+ // Note that not all values specified by JOSE are handled by this code. Only |
+ // handled values are listed. |
+ // - kty (Key Type) |
+ // +-------+--------------------------------------------------------------+ |
+ // | "RSA" | RSA [RFC3447] | |
+ // | "oct" | Octet sequence (used to represent symmetric keys) | |
+ // +-------+--------------------------------------------------------------+ |
+ // - use (Key Use) |
+ // +-------+--------------------------------------------------------------+ |
+ // | "enc" | encrypt and decrypt operations | |
+ // | "sig" | sign and verify (MAC) operations | |
+ // | "wrap"| key wrap and unwrap [not yet part of JOSE] | |
+ // +-------+--------------------------------------------------------------+ |
+ // - extractable (Key Exportability) |
+ // +-------+--------------------------------------------------------------+ |
+ // | true | Key may be exported from the trusted environment | |
+ // | false | Key cannot exit the trusted environment | |
+ // +-------+--------------------------------------------------------------+ |
+ // - alg (Algorithm) |
+ // See http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-16 |
+ // +--------------+-------------------------------------------------------+ |
+ // | Digital Signature or MAC Algorithm | |
+ // +--------------+-------------------------------------------------------+ |
+ // | "HS256" | HMAC using SHA-256 hash algorithm | |
+ // | "HS384" | HMAC using SHA-384 hash algorithm | |
+ // | "HS512" | HMAC using SHA-512 hash algorithm | |
+ // | "RS256" | RSASSA using SHA-256 hash algorithm | |
+ // | "RS384" | RSASSA using SHA-384 hash algorithm | |
+ // | "RS512" | RSASSA using SHA-512 hash algorithm | |
+ // +--------------+-------------------------------------------------------| |
+ // | Key Management Algorithm | |
+ // +--------------+-------------------------------------------------------+ |
+ // | "RSA1_5" | RSAES-PKCS1-V1_5 [RFC3447] | |
+ // | "RSA-OAEP" | RSAES using Optimal Asymmetric Encryption Padding | |
+ // | | (OAEP) [RFC3447], with the default parameters | |
+ // | | specified by RFC3447 in Section A.2.1 | |
+ // | "A128KW" | Advanced Encryption Standard (AES) Key Wrap Algorithm | |
+ // | | [RFC3394] using 128 bit keys | |
+ // | "A256KW" | AES Key Wrap Algorithm using 256 bit keys | |
+ // | "A128GCM" | AES in Galois/Counter Mode (GCM) [NIST.800-38D] using | |
+ // | | 128 bit keys | |
+ // | "A256GCM" | AES GCM using 256 bit keys | |
+ // | "A128CBC" | AES in Cipher Block Chaining Mode (CBC) with PKCS #5 | |
+ // | | padding [NIST.800-38A] [not yet part of JOSE] | |
+ // | "A256CBC" | AES CBC using 256 bit keys [not yet part of JOSE] | |
+ // | "A384CBC" | AES CBC using 384 bit keys [not yet part of JOSE] | |
+ // | "A512CBC" | AES CBC using 512 bit keys [not yet part of JOSE] | |
+ // +--------------+-------------------------------------------------------+ |
+ // |
+ // kty-specific parameters |
+ // The value of kty determines the type and content of the keying material |
+ // carried in the JWK to be imported. Currently only two possibilities are |
+ // supported: a raw key or an RSA public key. RSA private keys are not |
+ // supported because typical applications seldom need to import a private key, |
+ // and the large number of JWK parameters required to describe one. |
+ // - kty == "oct" (symmetric or other raw key) |
+ // +-------+--------------------------------------------------------------+ |
+ // | "k" | Contains the value of the symmetric (or other single-valued) | |
+ // | | key. It is represented as the base64url encoding of the | |
+ // | | octet sequence containing the key value. | |
+ // +-------+--------------------------------------------------------------+ |
+ // - kty == "RSA" (RSA public key) |
+ // +-------+--------------------------------------------------------------+ |
+ // | "n" | Contains the modulus value for the RSA public key. It is | |
+ // | | represented as the base64url encoding of the value's | |
+ // | | unsigned big endian representation as an octet sequence. | |
+ // +-------+--------------------------------------------------------------+ |
+ // | "e" | Contains the exponent value for the RSA public key. It is | |
+ // | | represented as the base64url encoding of the value's | |
+ // | | unsigned big endian representation as an octet sequence. | |
+ // +-------+--------------------------------------------------------------+ |
+ // |
+ // Conflict resolution |
+ // The type, algorithm, extractable, and usage_mask input parameters may be |
+ // different from similar values inside the JWK. Conflicts are resolved as |
+ // follows: |
+ // type: Deduce value from JWK contents only, ignore input value |
+ // algorithm: Use JWK value if present, otherwise use input value |
+ // extractable: If JWK value present, logical AND of the input and JWK values, |
+ // otherwise use input value |
+ // keyUsage: Use JWK value if present, otherwise use input value |
Ryan Sleevi
2013/10/04 23:19:11
These are all things that should be in the spec be
padolph
2013/10/05 02:57:42
The other parameters are easy, but can you please
|
+ |
+ std::string jsonStr(key_data, key_data + key_data_size); |
Ryan Sleevi
2013/10/04 23:19:11
json_str
However, you don't need to use std::stri
padolph
2013/10/05 02:57:42
Done.
|
+ scoped_ptr<base::Value> value(base::JSONReader::Read(jsonStr)); |
+ // Note, bare pointer dict_value is ok since it points into scoped value. |
+ base::DictionaryValue* dict_value = NULL; |
+ if (!value.get() || !value->GetAsDictionary(&dict_value) || !dict_value) |
+ return false; |
+ |
+ // JWK "kty". Exit early if this required JWK parameter is missing. |
+ std::string jwk_kty_value; |
+ if (!dict_value->GetString("kty", &jwk_kty_value)) |
+ return false; |
+ |
+ // JWK "extractable" --> extractable parameter |
+ bool jwk_extractable_value; |
+ if (dict_value->GetBoolean("extractable", &jwk_extractable_value)) { |
+ *extractable &= jwk_extractable_value; |
+ } |
+ |
+ // JWK "alg" --> algorithm parameter |
+ std::string jwk_alg_value; |
+ if (dict_value->GetString("alg", &jwk_alg_value)) { |
+ const StringIntMap::iterator pos = jwk_algorithm_map_.find(jwk_alg_value); |
+ if (pos == jwk_algorithm_map_.end()) |
+ return false; |
+ const int jwk_algorithm_id = pos->second; |
+ const unsigned short key_length_bits = |
+ JwkAlgIdToKeyLengthBits(jwk_algorithm_id); |
+ const WebKit::WebCryptoAlgorithmId webcrypto_algorithm_id = |
+ JwkAlgIdToWebCryptoAlgId(jwk_algorithm_id); |
+ switch (webcrypto_algorithm_id) { |
+ case WebKit::WebCryptoAlgorithmIdHmac: |
+ *algorithm = CreateHmacAlgorithm(key_length_bits); |
+ break; |
+ case WebKit::WebCryptoAlgorithmIdRsaSsaPkcs1v1_5: |
+ *algorithm = CreateRsaSsaAlgorithm(key_length_bits); |
+ break; |
+ case WebKit::WebCryptoAlgorithmIdRsaEsPkcs1v1_5: |
+ *algorithm = CreateAlgorithm(webcrypto_algorithm_id); |
+ break; |
+ case WebKit::WebCryptoAlgorithmIdRsaOaep: |
+ *algorithm = CreateRsaOaepAlgorithm(key_length_bits); |
+ break; |
+ case WebKit::WebCryptoAlgorithmIdAesGcm: |
+ *algorithm = CreateAesGcmAlgorithm(key_length_bits); |
+ break; |
+ case WebKit::WebCryptoAlgorithmIdAesCbc: |
+ *algorithm = CreateAesCbcAlgorithm(key_length_bits); |
+ break; |
+ default: |
+ return false; |
+ } |
+ } |
+ |
+ // JWK "use" --> usage_mask parameter |
+ std::string jwk_use_value; |
+ if (dict_value->GetString("use", &jwk_use_value)) { |
+ if (jwk_use_value == "enc") { |
+ *usage_mask = |
+ WebKit::WebCryptoKeyUsageEncrypt | WebKit::WebCryptoKeyUsageDecrypt; |
+ } else if (jwk_use_value == "sig") { |
+ *usage_mask = |
+ WebKit::WebCryptoKeyUsageSign | WebKit::WebCryptoKeyUsageVerify; |
+ } else if (jwk_use_value == "wrap") { |
+ *usage_mask = |
+ WebKit::WebCryptoKeyUsageWrapKey | WebKit::WebCryptoKeyUsageUnwrapKey; |
+ } else { |
+ return false; |
+ } |
+ } |
+ |
+ // JWK keying material --> ImportKeyInternal() |
+ if (jwk_kty_value == "oct") { |
+ std::string jwk_k_value_url64; |
+ if (!dict_value->GetString("k", &jwk_k_value_url64)) |
+ return false; |
+ std::string jwk_k_value; |
+ if (!Base64DecodeUrlSafe(jwk_k_value_url64, &jwk_k_value)) |
+ return false; |
+ const std::vector<uint8> data(jwk_k_value.begin(), jwk_k_value.end()); |
+ if (!ImportKeyInternal(WebKit::WebCryptoKeyFormatRaw, |
+ &data[0], |
+ data.size(), |
+ *algorithm, |
+ *usage_mask, |
+ handle, |
+ type)) |
+ return false; |
+ } else if (jwk_kty_value == "RSA") { |
+ // TODO(padolph): RSA public key |
+ return false; |
+ } else { |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
} // namespace content |