# CyberConnect功能逆向与EasyCyberConnect的实现

By [PictorHugo](https://paragraph.com/@pictorhugo) · 2022-05-29

---

前言
--

在我学过的知识中，计算机知识是最开放最容易获取的。在计算机领域中，前端技术又是更开放的一个存在，因为浏览器的存在，你可以随意查看一些大项目的前端源代码而不需要谁给你任何权限，故在前端领域中逆向某个项目是最容易的了。逆向这个词，可以简单理解为根据结果推导过程，一般解密也就是对未开放源码的程序进行的，因为没有源码，我们只能对程序本身进行解密等操作，从而猜测其实现是什么。今天的主角CyberConnect作为一个社交图谱的web3应用，目前其对用户主要开放的就是[cyberconnect.me](http://cyberconnect.me)这个站点。逆向部分就是cyberconnect的follow，查看follwers、followings，注册等功能。根据逆向的代码还实现了一个简易版本的cyberconnect前端，链接如下。

[https://easycyberconnect.000webhostapp.com/](https://easycyberconnect.000webhostapp.com/)

目前支持搜索某个地址所有的followers、followings，不只是前50个哦，而且支持follow那些未关注的followers。假设你是一个大v，有很多人关注（followers可能达到了10000+），现在你想回关一些followers，现在无论是通过cyberconnect.me还是galaxy，还是

[http://47.102.205.94:2000/](http://47.102.205.94:2000/)

目前都没办法很方便的去操作。我上面的easycyberconnect也就是解决了这个痛点。

除了解决自己的痛点外，自己做这个事情也是以学习为目的。自己作为一个年轻的程序员，对任何新技术都还抱有着热情，而且光有热情还是不够的，还要参与进来。那么通过一个个小的项目，从做中学，是一个很好的开始。希望自己可以坚持，我也会将自己的学习过程分享给大家。

EasyCyberConnect开源
------------------

[https://github.com/PictorHugo/EasyCyberConnect](https://github.com/PictorHugo/EasyCyberConnect)

大部分朋友可能并不关注实现的细节，这里我把源代码开源出来，从而也让大家使用起来更放心。因为这个项目只是一个纯前端的实现，我将它架设在了000webhostapp上，这样也不需要掏任何服务器费用。想尝试丝滑般关注体验的朋友，可以尝试下下面的链接。

[https://easycyberconnect.000webhostapp.com/](https://easycyberconnect.000webhostapp.com/)

EasyCyberConnect源码逆向
--------------------

这一部分记录了我对cyberconnect的研究，属于自己对学习过程的一个记录与整理。不感兴趣的朋友可以直接跳过。

在逆向前我要划定自己逆向的范围，因为cyberconnect确实是个很庞大的项目，像无头苍蝇一样到处看，必定是不行的。我逆向的`目的`如下：

*   查看超过top 50外的followers和followings
    
*   如何follow某个用户
    

确定了范围，接下来就要看具体方法了。从程序员的角度看，cyberconnect app也就是客户端就是一个前端程序，而且现在普遍的前端与后端交互的方式就是[REST API](https://restfulapi.net/)。那么，我们只要获得，上面`目的` 中对应功能的REST API，并且了解REST API的各个参数是如何获得的，那么这个任务也就完成了。

### Task 1: 查看超过top 50外的followers和followings

我们从galaxy的前端入手（原因是galaxy比较快一点），先了解下是如何获得top 50的followers的，那么也许可以猜测出如何查看超过top50的followers。这里就要利用chrome的一个强大的抓包功能（在galaxy.eco页面上单击鼠标右键 → 点击inspect或者检查 → 切换到Network或者网络），通过这个抓包功能我们能看到每个从前端发送出去的HTTP/HTTPs请求，那么只要耐心，我们一定可以找到一些线索。

下面就是我找到本次任务中API的过程

*   打开浏览器抓包功能
    
*   打开galaxy.eco/galaxyid这个页面
    
*   查看每一个请求的响应，看看是否有包含followers的信息
    

![galaxy用于显示top 50 followers/follwings的API](https://storage.googleapis.com/papyrus_images/b7fe22058fefe15c37db2bf00fdcac42ad1f4ce96719cb69e1735c6b88d767ea.png)

galaxy用于显示top 50 followers/follwings的API

通过以上方法，和一点耐心，终于找到了这个API，具体的API格式如下。这里可以看到比较重要的就是API BODY部分，如果理解了这一部分那么完成本task就不是问题了。API BODY又分为两个部分，一部分是query，这里使用的一种与[graphql](https://graphql.org/graphql-js/)相关的语句，具体的作用就是查询后端服务器的哪些数据，因为我们没法看到后端的实现是什么，那么这一部分大概率先保持原状。初步理解这个graphql语句就当成一个函数，我们给它某些输入参数，这个函数就会返回一个JSON结构的数据，里面包含有followers和followings。另一部分是variables，目前看里面只有两个参数，分别是address和first，address就是当前的cyberconnect账户，first结合response结果也很好理解，也就是显示前50个数据。那么到这里我们也就有个想法，是不是修改first对应的值就可以了呢？比如改成100？我尝试后改成100还是不行，仍然只显示前50个数据。不过通过抓包发现cyberconnect的主站点上，在调用这个API的时候还多加了一个参数after，这个成为了本问题的突破口。

REST API请求：

    HTTP METHOD: POST
    API PATH: api.cybertino.io/connect
    API BODY:
    {
        "query": "query GetIdentity($address: String!, $first: Int, $after: String) {\n  identity(address: $address) {\n    address\n    ens\n     social {\n      twitter\n      __typename\n    }\n    followerCount(namespace: \"\")\n    followingCount(namespace: \"\")\n followers(first: $first, after: $after, namespace: \"\") {\n      pageInfo {\n        ...PageInfo\n        __typename\n      }\n      list {\n        ...Connect\n        __typename\n      }\n      __typename\n    }\n  __typename\n  }\n}\n\nfragment PageInfo on PageInfo {\n  startCursor\n  endCursor\n  hasNextPage\n  hasPreviousPage\n  __typename\n}\n\nfragment Connect on Connect {\n  address\n  ens\n  alias\n  namespace\n  __typename\n}\n",
        "variables": {
            "address": "0x037aa2a37b06699f9ea40c4830275620d367fefa",
            "first": 50
        }
    }
    

REST API响应 (… 代表下面内容基本与上面一致，为了简洁考虑所以省略了)：

    {
        "data": {
            "identity": {
                "address": "0x037aa2a37b06699f9ea40c4830275620d367fefa",
                "ens": "pictorhugo.eth",
                "social": {
                    "twitter": "PictorHugo",
                    "__typename": "Social"
                },
                "followerCount": 196,
                "followingCount": 238,
                "followings": {
                    "pageInfo": {
                        "startCursor": "0",
                        "endCursor": "49",
                        "hasNextPage": true,
                        "hasPreviousPage": false,
                        "__typename": "PageInfo"
                    },
                    "list": [
                        {
                            "address": "0xeefc6347cb77bbc878dd4ea0a616da69d7bc67f3",
                            "ens": "",
                            "alias": "",
                            "namespace": "Galaxy",
                            "__typename": "Connect"
                        },
                        ...
                    ],
                    "__typename": "ConnectionIdentityPage"
                },
                "followers": {
                    "pageInfo": {
                        "startCursor": "0",
                        "endCursor": "49",
                        "hasNextPage": true,
                        "hasPreviousPage": false,
                        "__typename": "PageInfo"
                    },
                    "list": [
                        {
                            "address": "0xee857065cbe599febfa772e96c17996deacd7830",
                            "ens": "",
                            "alias": "",
                            "namespace": "CyberConnect",
                            "__typename": "Connect"
                        },
                        ...
                    ],
                    "__typename": "ConnectionIdentityPage"
                },
                "__typename": "UserIdentity"
            }
        }
    }
    

这个就是从cyberconnect上查找到带after参数的接口，其实这个信息也可以通过[graphql](https://graphql.org/graphql-js/)语句得到，但因为一开始不是很熟悉它的语法没有注意到。

![带有after参数的请求](https://storage.googleapis.com/papyrus_images/d84d05691a300effa0cab8f4c076399664b2bd9a206af28d1c1098b090f9eea7.png)

带有after参数的请求

经过一些尝试，这个after参数里填写的也是一个地址string，而且通过这个after就可以实现翻页功能。具体意思就是，当after为空的时候，查找前first个数据，假设分别是，follower1，follower2，… folower50。在下一次执行查询语句的时候，将上一次的follower50的address填入after，first仍然是50，那么就可以查找到top50后面的数据。

### Task 2: 如何follow某个用户

想获取follow某个用户的API很容易，不用像task1里那样，从整个抓包数据中一点点过滤。因为我们只需要触发一下follow这个操作，那么接下来抓去到的请求一定与follow操作相关。抓去到的API如下。

REST API请求：

    HTTP METHOD: POST
    API PATH: api.cybertino.io/connect
    API BODY:
    {
        "operationName": "connect",
        "query": "mutation connect($input: UpdateConnectionInput!) {connect(input: $input) {result}}",
        "variables": {
            "input": {
                "fromAddr": "0x037aa2a37b06699f9ea40c4830275620d367fefa",
                "toAddr": "0xf601d1f8e82fc71514e88bc2b0a26d8a305f9985",
                "alias": "",
                "namespace": "Galaxy",
                "signature": "...",
                "signingKey": "...",
                "operation": "{\"name\":\"follow\",\"from\":\"0x037aa2a37b06699f9ea40c4830275620d367fefa\",\"to\":\"0xf601d1f8e82fc71514e88bc2b0a26d8a305f9985\",\"namespace\":\"Galaxy\",\"network\":\"ETH\",\"timestamp\":1653799783858,\"alias\":\"\"}",
                "network": "ETH",
                "type": "FOLLOW"
            }
        }
    }
    

REST API响应：

    {
        "data": {
            "connect": {
                "result": "SUCCESS"
            }
        }
    }
    

这个API的获取难度非常低，但难点在于如何知道API请求中各个参数都应该怎样填写。下面让我们来按个分析一下。

operation，的值为connect，这应该代表follow操作的意思，因为我也同时抓去到了unfollow的请求，其中operation的值为disconnect。

query，其值依然是一个graphql语句，我们暂且不去理解细节。

下面分析variables中input里的各个参数。

fromAddr，其值是连接cyberconnect的钱包地址

toAddr，将要关注的钱包地址

alias，暂时不清楚作用

signature，将某些数据经过浏览器生成的私钥(为了方便，后面称其为私钥a)签名后的字符串，具体计算逻辑查看下面\`signature与signingKey计算逻辑\`那一节。

signingKey，私钥a对应的公钥经过hash后的值，具体可以查看\`signature与signingKey计算逻辑\`。

operation，包括follow动作的发起人，将要follow的人等信息，具体和上面介绍过的都差不多，只是没有signature和signingKey等参数。

network，目前应该可以填写ETH和Solana两种。

type，是操作的名字。

浏览完上面每个参数的介绍后，我们可以发现，大部分参数都是很好获取的，比如network，type等，都是固定填写就好。但signingKey与signature就不太好理解了，下面让我们重点关注这两个参数。

### signature与signingKey计算逻辑

为了获得这两个参数的计算逻辑，我使用了两种方法，一种是归纳法，一种是查看源码法。

归纳法：

我尝试了多次的follow操作，发现signingKey是一直不变的，从而可以断定这是一个存储在本地的值，那么也就很有可能是连接本网站的钱包地址经过hash后得到的值（后面被证明是错误的，请继续看）。但是signature却次次不一样，一种可能的猜测是，每次计算签名的时候加入了时间戳信息。

查看源码法： 光靠归纳的方法去猜测肯定是不行了，还是得看源码，这也是前端代码的一个好处，那么接下来的问题就是：

*   查看哪个具体文件的源码
    
*   如何看源码
    

第一个问题，chrome的抓包功能帮我们解决了，下面截图中有一列叫做initiator，这里就代表了这个请求发起的时候所对应的代码。

![查看请求对应文件](https://storage.googleapis.com/papyrus_images/ea0e3c8d5d75e5b10d49f7b532db6f8c1372395c06057cc13abeec9116f16276.png)

查看请求对应文件

![源码文件](https://storage.googleapis.com/papyrus_images/45570b2224bbad8f1cbc5e9eac59b5377f8e2008d586869989f09b841cf66fcd.png)

源码文件

但当我们打开这个源码的时候却发现是这样的，基本上不具有任何可读性，这个时候我们可以通过点击左下角的按钮从而格\`{}\`式化源码文件，但也只能是让格式变的好看点，各种变量名仍然是不具有可读性的。下面就只能靠耐性了，说实在的，接下来是有点枯燥的，因为只能一点点去钻研这类minified（前端的代码压缩）过的代码，我下面抽取一段，分享一下基本的思路。下面就是当用户出发follow按键的时候会触发的函数，其核心逻辑就在switch case语句中，如果熟悉js的async/await代码实现原理的朋友看下面的代码应该并不陌生，或者大家可以查看下面这篇文章。其是就是一种状态机的转换。我下面实际的说一下，首先当a.lable为0的时候执行相应的语句，其返回值为this.getAddress()，当a.label为1的时候执行相应的语句，并且通过a.sent可以获得a.label为0时候的返回值，接下来就以此类推。

[http://codereform.com/blog/post/es6-generators-and-asyncawait-in-typescript/](http://codereform.com/blog/post/es6-generators-and-asyncawait-in-typescript/)

    e.prototype.connect = function(e, t, n) {
                    var r, o, s, c;
                    return void 0 === t && (t = ""),
                    void 0 === n && (n = x.ConnectionType.FOLLOW),
                    i(this, void 0, void 0, (function() {
                        var i, u, f, d, l, h, b;
                        return a(this, (function(a) {
                            switch (a.label) {
                            case 0:
                                return a.trys.push([0, 8, , 9]),
                                i = this,
                                [4, this.getAddress()];
                            case 1:
                                return i.address = a.sent(),
                                [4, this.authWithSigningKey()];
                            case 2:
                                return a.sent(),
                                u = {
                                    name: n.toLowerCase(),
                                    from: this.address,
                                    to: e,
                                    namespace: this.namespace,
                                    network: this.chain,
                                    timestamp: Date.now(),
                                    alias: t
                                },
                                [4, (0,
                                E.signWithSigningKey)(JSON.stringify(u), this.address)];
                            case 3:
                                return f = a.sent(),
                                [4, (0,
                                E.getPublicKey)(this.address)];
                            case 4:
                                return d = a.sent(),
                                l = {
                                    fromAddr: this.address,
                                    toAddr: e,
                                    alias: t,
                                    namespace: this.namespace,
                                    signature: f,
                                    signingKey: d,
                                    operation: JSON.stringify(u),
                                    network: this.chain,
                                    type: n
                                },
                                [4, (0,
                                v.follow)(l, this.endpoint.cyberConnectApi)];
                            case 5:
                                return h = a.sent(),
                                "INVALID_SIGNATURE" !== (null === (r = null === h || void 0 === h ? void 0 : h.data) || void 0 === r ? void 0 : r.connect.result) ? [3, 7] : [4, (0,
                                E.clearSigningKey)()];
                            case 6:
                                throw a.sent(),
                                new y.ConnectError(y.ErrorCode.GraphqlError,null === (o = null === h || void 0 === h ? void 0 : h.data) || void 0 === o ? void 0 : o.connect.result);
                            case 7:
                                if ("SUCCESS" !== (null === (s = null === h || void 0 === h ? void 0 : h.data) || void 0 === s ? void 0 : s.connect.result))
                                    throw new y.ConnectError(y.ErrorCode.GraphqlError,null === (c = null === h || void 0 === h ? void 0 : h.data) || void 0 === c ? void 0 : c.connect.result);
                                return [3, 9];
                            case 8:
                                throw b = a.sent(),
                                new y.ConnectError(y.ErrorCode.GraphqlError,b.message || b);
                            case 9:
                                return M.DFLAG && this.ceramicConnect(e, t),
                                [2]
                            }
                        }
                        ))
                    }
                    ))
                }
    

通过上面的方式，我通读了这一部分的代码，总结了一个follow过程的流程图给大家。具体的代码大家可以查我已经开源的代码。

![获取signature signingKey的流程](https://storage.googleapis.com/papyrus_images/3c690c651f04ff79a73281090d81406a117410d85810ae5f97457465566d6b3e.png)

获取signature signingKey的流程

研究这一部分后，主要获得一个信息，就是对数据进行签名的私钥并不是连接钱包的账户的私钥，而是前端自己生成的一个公钥和私钥对。这样确实也能让follow操作更丝滑一点，因为每次都要用户自己签名一次就去follow一个人还是挺费劲的。

### key的注册

在我以为理解了signature的计算后就能自己脱离cyberconnect前端去follow用户的时候，我发现cyberconnect还是返回了一个error，不能让我成功follow，仔细推测后，发现有个流程被我漏掉了，就是当前端无法从indexed db超找到keypair的时候其实是会发送一个registerKey的请求，因为我漏掉了这个请求所以follow会失败。下面就是register key的REST API。

REST API请求：

    HTTP METHOD: POST
    API PATH: api.cybertino.io/connect
    API BODY:
    {
        "operationName": "registerKey",
        "query": "mutation registerKey($input: RegisterKeyInput!) {\n      registerKey(input: $input) {\n        result\n      }\n    }",
        "variables": {
            "input": {
                "address": "0x037aa2a37b06699f9ea40c4830275620d367fefa",
                "message": "I authorize CyberConnect from this device using signing key:....",
                "signature": "....",
                "network": "ETH"
            }
        }
    }
    

那么到此，我们也基本上理解了如何去通过REST API去follow一个用户。

---

*Originally published on [PictorHugo](https://paragraph.com/@pictorhugo/cyberconnect-easycyberconnect)*
