ErrorType decrypt(unsigned char* packet,
                    uint32_t packet_len,
                    uint32_t seqno,
                    unsigned char* first_block,
                    uint32_t first_block_len,
                    unsigned char* mac,
                    uint32_t mac_len) {
    ErrorType r = kErrNone;

    int outlen;
    unsigned char calc_mac[hmac_len_] = {0};

    uint8_t seqbuf[4] = {0};
    ((uint8_t*)(seqbuf))[0] = (seqno >> 24) & 0xff;
    ((uint8_t*)(seqbuf))[1] = (seqno >> 16) & 0xff;
    ((uint8_t*)(seqbuf))[2] = (seqno >> 8) & 0xff;
    ((uint8_t*)(seqbuf))[3] = seqno & 0xff;

    if (is_etm_) {
      // `first_block` for ETM should just be the unencrypted packet length
      if (first_block_len != 4) {
        r = kErrBadBlockLen;
        goto out;
      }

      // HMAC over unencrypted packet length and ciphertext
      {
        unsigned int outlen = hmac_len_;
        if (HMAC_Init_ex(ctx_hmac_, nullptr, 0, nullptr, nullptr) != 1
            || HMAC_Update(ctx_hmac_, seqbuf, sizeof(seqbuf)) != 1
            || HMAC_Update(ctx_hmac_, first_block, first_block_len) != 1
            || HMAC_Update(ctx_hmac_, packet, packet_len) != 1
            || HMAC_Final(ctx_hmac_, calc_mac, &outlen) != 1) {
          r = kErrOpenSSL;
          goto out;
        }

        if (outlen != hmac_len_ || mac_len != hmac_len_) {
          r = kErrBadHMACLen;
          goto out;
        }

        // Compare MACs
        if (CRYPTO_memcmp(mac, calc_mac, sizeof(calc_mac))) {
          r = kErrInvalidMAC;
          goto out;
        }
      }

      // Decrypt packet
      if (EVP_DecryptUpdate(ctx_, packet, &outlen, packet, packet_len) != 1) {
        r = kErrOpenSSL;
        goto out;
      }
      if (static_cast<size_t>(outlen) != packet_len) {
        r = kErrPartialDecrypt;
        goto out;
      }
    } else {
      // `first_block` for non-ETM should be a completely decrypted first block
      if (!is_stream_ && first_block_len != block_size_) {
        r = kErrBadBlockLen;
        goto out;
      }

      const int offset = (is_stream_ ? 0 : block_size_ - 4);
      // Decrypt the rest of the packet
      if (EVP_DecryptUpdate(ctx_,
                            packet + offset,
                            &outlen,
                            packet + offset,
                            packet_len - offset) != 1) {
        r = kErrOpenSSL;
        goto out;
      }
      if (static_cast<size_t>(outlen) != packet_len - offset) {
        r = kErrPartialDecrypt;
        goto out;
      }

      // HMAC over plaintext
      {
        unsigned int outlen = hmac_len_;
        if (HMAC_Init_ex(ctx_hmac_, nullptr, 0, nullptr, nullptr) != 1
            || HMAC_Update(ctx_hmac_, seqbuf, sizeof(seqbuf)) != 1
            || HMAC_Update(ctx_hmac_, first_block, first_block_len) != 1
            || HMAC_Update(ctx_hmac_, packet, packet_len) != 1
            || HMAC_Final(ctx_hmac_, calc_mac, &outlen) != 1) {
          r = kErrOpenSSL;
          goto out;
        }

        if (outlen != hmac_len_ || mac_len != hmac_actual_len_) {
          r = kErrBadHMACLen;
          goto out;
        }

        // Compare MACs
        if (CRYPTO_memcmp(mac, calc_mac, hmac_actual_len_)) {
          r = kErrInvalidMAC;
          goto out;
        }
      }
    }

out:
    return r;
  }