Commit message: Fix potential security leak in HashContext

Summary: CVE-2014-6229

This is not a NUL-terminated string, it's a fixed-size block of data.
The risks were key truncation (if there happens to be a NUL byte in the
key) or over-reading (which would be information leakage).

Reviewed By: @ptarjan

Differential Revision: D1533546
CVE: CVE-2014-6229
CWE: CWE-200
Vulnerability description: The HashContext class in hphp/runtime/ext/ext_hash.cpp in Facebook HipHop Virtual Machine (HHVM) before 3.3.0 incorrectly expects that a certain key string uses '\0' for termination, which allows remote attackers to obtain sensitive information by leveraging read access beyond the end of the string, and makes it easier for remote attackers to defeat cryptographic protection mechanisms by leveraging truncation of a string containing an internal '\0' character.

Project: hhvm
File: ext_hash.cpp

Variant HHVM_FUNCTION(hash_final, const Resource& context,
                                 bool raw_output /* = false */) {
  HashContext *hash = context.getTyped<HashContext>();

  if (hash->context == nullptr) {
    raise_warning(
      "hash_final(): supplied resource is not a valid Hash Context resource"
    );
    return false;
  }

  String raw = String(hash->ops->digest_size, ReserveString);
  char *digest = raw.bufferSlice().ptr;
  hash->ops->hash_final((unsigned char *)digest, hash->context);
  if (hash->options & k_HASH_HMAC) {
    finalize_hmac_key(hash->key, hash->ops, hash->context, digest);
    hash->key = NULL;
  }
  free(hash->context);
  hash->context = NULL;

  raw.setSize(hash->ops->digest_size);
  if (raw_output) {
    return raw;
  }
  return f_bin2hex(raw);
}

Project: hhvm
File: ext_hash.cpp

static Variant php_hash_do_hash(const String& algo, const String& data,
                                bool isfilename,
                                bool raw_output) {
  HashEnginePtr ops = php_hash_fetch_ops(algo);
  if (!ops) {
    raise_warning("Unknown hashing algorithm: %s", algo.data());
    return false;
  }
  Variant f;
  if (isfilename) {
    f = f_fopen(data, "rb");
    if (same(f, false)) {
      return false;
    }
  }

  void *context = malloc(ops->context_size);
  ops->hash_init(context);

  if (isfilename) {
    for (Variant chunk = f_fread(f.toResource(), 1024);
         !is_empty_string(chunk);
         chunk = f_fread(f.toResource(), 1024)) {
      String schunk = chunk.toString();
      ops->hash_update(context, (unsigned char *)schunk.data(), schunk.size());
    }
  } else {
    ops->hash_update(context, (unsigned char *)data.data(), data.size());
  }

  String raw = String(ops->digest_size, ReserveString);
  char *digest = raw.bufferSlice().ptr;
  ops->hash_final((unsigned char *)digest, context);
  free(context);

  raw.setSize(ops->digest_size);
  if (raw_output) {
    return raw;
  }
  return f_bin2hex(raw);
}

Project: hhvm
File: ext_hash.cpp

static void finalize_hmac_key(char *K, HashEnginePtr ops, void *context,
                              char *digest) {
  /* Convert K to opad -- 0x6A = 0x36 ^ 0x5C */
  for (int i = 0; i < ops->block_size; i++) {
    K[i] ^= 0x6A;
  }

  /* Feed this result into the outter hash */
  ops->hash_init(context);
  ops->hash_update(context, (unsigned char *)K, ops->block_size);
  ops->hash_update(context, (unsigned char *)digest, ops->digest_size);
  ops->hash_final((unsigned char *)digest, context);

  /* Zero the key */
  memset(K, 0, ops->block_size);
  free(K);
}

Project: hhvm
File: ext_hash.cpp

  HashExtension() : Extension("hash", "1.0") { }

Project: hhvm
File: ext_hash.cpp

Array HHVM_FUNCTION(hash_algos) {
  Array ret;
  for (HashEngineMap::const_iterator iter = HashEngines.begin();
       iter != HashEngines.end(); ++iter) {
    ret.append(String(iter->first));
  }
  return ret;
}

Project: hhvm
File: ext_hash.cpp

  virtual const String& o_getClassNameHook() const { return classnameof(); }

Project: hhvm
File: ext_hash.cpp

int64_t HHVM_FUNCTION(furchash_hphp_ext, const String& key,
                                         int64_t len, int64_t nPart) {
  len = std::max<int64_t>(std::min<int64_t>(len, key.size()), 0);
  return furc_hash(key.data(), len, nPart);
}

Project: hhvm
File: ext_hash.cpp

Resource HHVM_FUNCTION(hash_copy, const Resource& context) {
  HashContext *oldhash = context.getTyped<HashContext>();
  auto const hash = new HashContext(oldhash);
  return Resource(hash);
}

Project: hhvm
File: ext_hash.cpp

  virtual void moduleLoad(const IniSetting::Map& ini, Hdf config) {
    HHVM_FE(hash);
    HHVM_FE(hash_algos);
    HHVM_FE(hash_file);
    HHVM_FE(hash_final);
    HHVM_FE(hash_init);
    HHVM_FE(hash_update);
    HHVM_FE(hash_copy);
    HHVM_FE(hash_equals);
    HHVM_FE(furchash_hphp_ext);
    HHVM_FE(hphp_murmurhash);
  }

Project: hhvm
File: ext_hash.cpp

static HashEnginePtr php_hash_fetch_ops(const String& algo) {
  HashEngineMap::const_iterator iter =
    HashEngines.find(f_strtolower(algo).data());
  if (iter == HashEngines.end()) {
    return HashEnginePtr();
  }
  return iter->second;
}

Project: hhvm
File: ext_hash.cpp

Variant HHVM_FUNCTION(hash, const String& algo, const String& data,
                            bool raw_output /* = false */) {
  return php_hash_do_hash(algo, data, false, raw_output);
}

Project: hhvm
File: ext_hash.cpp

  ~HashContext() {
    HashContext::sweep();
  }

Project: hhvm
File: ext_hash.cpp

  HashContext(HashEnginePtr ops_, void *context_, int options_)
    : ops(ops_), context(context_), options(options_), key(nullptr) {
  }

Project: hhvm
File: ext_hash.cpp

Variant HHVM_FUNCTION(hash_init, const String& algo,
                                 int64_t options /* = 0 */,
                                 const String& key /* = null_string */) {
  HashEnginePtr ops = php_hash_fetch_ops(algo);
  if (!ops) {
    raise_warning("Unknown hashing algorithm: %s", algo.data());
    return false;
  }

  if ((options & k_HASH_HMAC) && key.empty()) {
    raise_warning("HMAC requested without a key");
    return false;
  }

  void *context = malloc(ops->context_size);
  ops->hash_init(context);

  const auto hash = new HashContext(ops, context, options);
  if (options & k_HASH_HMAC) {
    hash->key = prepare_hmac_key(ops, context, key);
  }
  return Resource(hash);
}

Project: hhvm
File: ext_hash.cpp

  HashEngineMapInitializer() {
    HashEngines["md2"]        = HashEnginePtr(new hash_md2());
    HashEngines["md4"]        = HashEnginePtr(new hash_md4());
    HashEngines["md5"]        = HashEnginePtr(new hash_md5());
    HashEngines["sha1"]       = HashEnginePtr(new hash_sha1());
    HashEngines["sha224"]     = HashEnginePtr(new hash_sha224());
    HashEngines["sha256"]     = HashEnginePtr(new hash_sha256());
    HashEngines["sha384"]     = HashEnginePtr(new hash_sha384());
    HashEngines["sha512"]     = HashEnginePtr(new hash_sha512());
    HashEngines["ripemd128"]  = HashEnginePtr(new hash_ripemd128());
    HashEngines["ripemd160"]  = HashEnginePtr(new hash_ripemd160());
    HashEngines["ripemd256"]  = HashEnginePtr(new hash_ripemd256());
    HashEngines["ripemd320"]  = HashEnginePtr(new hash_ripemd320());
    HashEngines["whirlpool"]  = HashEnginePtr(new hash_whirlpool());
#ifdef FACEBOOK
    // The original version of tiger got the endianness backwards
    // This fb-specific version remains for backward compatibility
    HashEngines["tiger128,3-fb"]
                              = HashEnginePtr(new hash_tiger(true, 128, true));
#endif
    HashEngines["tiger128,3"] = HashEnginePtr(new hash_tiger(true, 128));
    HashEngines["tiger160,3"] = HashEnginePtr(new hash_tiger(true, 160));
    HashEngines["tiger192,3"] = HashEnginePtr(new hash_tiger(true, 192));
    HashEngines["tiger128,4"] = HashEnginePtr(new hash_tiger(false, 128));
    HashEngines["tiger160,4"] = HashEnginePtr(new hash_tiger(false, 160));
    HashEngines["tiger192,4"] = HashEnginePtr(new hash_tiger(false, 192));

    HashEngines["snefru"]     = HashEnginePtr(new hash_snefru());
    HashEngines["gost"]       = HashEnginePtr(new hash_gost());
#ifdef FACEBOOK
    // Temporarily leave adler32 algo inverting its hash output
    // to retain BC pending conversion of user code to correct endianness
    // sgolemon(2014-01-30)
    HashEngines["adler32-fb"] = HashEnginePtr(new hash_adler32(true));
    HashEngines["adler32"]    = HashEnginePtr(new hash_adler32(true));
#else
    HashEngines["adler32"]    = HashEnginePtr(new hash_adler32());
#endif
    HashEngines["crc32"]      = HashEnginePtr(new hash_crc32(false));
    HashEngines["crc32b"]     = HashEnginePtr(new hash_crc32(true));
    HashEngines["haval128,3"] = HashEnginePtr(new hash_haval(3,128));
    HashEngines["haval160,3"] = HashEnginePtr(new hash_haval(3,160));
    HashEngines["haval192,3"] = HashEnginePtr(new hash_haval(3,192));
    HashEngines["haval224,3"] = HashEnginePtr(new hash_haval(3,224));
    HashEngines["haval256,3"] = HashEnginePtr(new hash_haval(3,256));
    HashEngines["haval128,4"] = HashEnginePtr(new hash_haval(4,128));
    HashEngines["haval160,4"] = HashEnginePtr(new hash_haval(4,160));
    HashEngines["haval192,4"] = HashEnginePtr(new hash_haval(4,192));
    HashEngines["haval224,4"] = HashEnginePtr(new hash_haval(4,224));
    HashEngines["haval256,4"] = HashEnginePtr(new hash_haval(4,256));
    HashEngines["haval128,5"] = HashEnginePtr(new hash_haval(5,128));
    HashEngines["haval160,5"] = HashEnginePtr(new hash_haval(5,160));
    HashEngines["haval192,5"] = HashEnginePtr(new hash_haval(5,192));
    HashEngines["haval224,5"] = HashEnginePtr(new hash_haval(5,224));
    HashEngines["haval256,5"] = HashEnginePtr(new hash_haval(5,256));
    HashEngines["fnv132"]     = HashEnginePtr(new hash_fnv132(false));
    HashEngines["fnv1a32"]    = HashEnginePtr(new hash_fnv132(true));
    HashEngines["fnv164"]     = HashEnginePtr(new hash_fnv164(false));
    HashEngines["fnv1a64"]    = HashEnginePtr(new hash_fnv164(true));
  }

Project: hhvm
File: ext_hash.cpp

  void sweep() FOLLY_OVERRIDE {
    /* Just in case the algo has internally allocated resources */
    if (context) {
      assert(ops->digest_size >= 0);
      unsigned char dummy[ops->digest_size];
      ops->hash_final(dummy, context);
      free(context);
    }

    free(key);
  }

Project: hhvm
File: ext_hash.cpp

Variant HHVM_FUNCTION(hash_file, const String& algo, const String& filename,
                                 bool raw_output /* = false */) {
  if (filename.size() != strlen(filename.data())) {
    raise_warning(
     "hash_file() expects parameter 2 to be a valid path, string given"
    );
    return null_variant;
  }
  return php_hash_do_hash(algo, filename, true, raw_output);
}

Project: hhvm
File: ext_hash.cpp

bool HHVM_FUNCTION(hash_update, const Resource& context, const String& data) {
  HashContext *hash = context.getTyped<HashContext>();
  hash->ops->hash_update(hash->context, (unsigned char *)data.data(),
                         data.size());
  return true;
}

Project: hhvm
File: ext_hash.cpp

  explicit HashContext(const HashContext* ctx) {
    assert(ctx->ops);
    assert(ctx->ops->context_size >= 0);
    ops = ctx->ops;
    context = malloc(ops->context_size);
    ops->hash_copy(context, ctx->context);
    options = ctx->options;
    if (ctx->key) {
      key = static_cast<char*>(malloc(ops->block_size));
      memcpy(key, ctx->key, ops->block_size);
    } else {
      key = nullptr;
    }
  }

Project: hhvm
File: ext_hash.cpp

static char *prepare_hmac_key(HashEnginePtr ops, void *context,
                              const String& key) {
  char *K = (char*)malloc(ops->block_size);
  memset(K, 0, ops->block_size);
  if (key.size() > ops->block_size) {
    /* Reduce the key first */
    ops->hash_update(context, (unsigned char *)key.data(), key.size());
    ops->hash_final((unsigned char *)K, context);
    /* Make the context ready to start over */
    ops->hash_init(context);
  } else {
    memcpy(K, key.data(), key.size());
  }

  /* XOR ipad */
  for (int i = 0; i < ops->block_size; i++) {
    K[i] ^= 0x36;
  }
  ops->hash_update(context, (unsigned char *)K, ops->block_size);
  return K;
}

Project: hhvm
File: ext_hash.cpp

bool HHVM_FUNCTION(hash_equals, const Variant& known, const Variant& user) {
  if (!known.isString()) {
    raise_warning(
      "hash_equals(): Expected known_string to be a string, %s given",
      getDataTypeString(known.getType()).c_str()
    );
    return false;
  }
  if (!user.isString()) {
    raise_warning(
      "hash_equals(): Expected user_string to be a string, %s given",
      getDataTypeString(user.getType()).c_str()
    );
    return false;
  }
  String known_str = known.toString();
  String user_str = user.toString();
  const auto known_len = known_str.size();
  const auto known_limit = known_len - 1;
  const auto user_len = user_str.size();
  int64_t result = known_len ^ user_len;

  int64_t ki = 0;
  for (int64_t ui = 0; ui < user_len; ++ui) {
    result |= user_str[ui] ^ known_str[ki];
    if (ki < known_limit) {
      ++ki;
    }
  }
  return (result == 0);
}

Project: hhvm
File: ext_hash.cpp

int64_t HHVM_FUNCTION(hphp_murmurhash, const String& key,
                                       int64_t len, int64_t seed) {
  len = std::max<int64_t>(std::min<int64_t>(len, key.size()), 0);
  return murmur_hash_64A(key.data(), len, seed);
}