解密某乎的电子书加密算法

151 阅读15分钟

背景

最近想在 wrx 项目中接入某乎的电子书导出,于是便逆向了一下该平台的电子书加解密逻辑。整个逻辑模仿了https的加解密过程,通过公钥加密对称加密所用的密码,然后用该密码进行加解密。与某信读书相比,无法通过拦截接口进行数据的解密,必须拿到客户端生成的临时密码才行。

接口分析

元数据接口

接口地址:

GET https://www.zhihu.com/api/v3/books/[bookId]?include=
查看响应示例
{
    "read_count": 10618,
    "right": {
        "raw_value": 0,
        "left_top_day_icon": "vip_free_day_icon",
        "main_title": "加入知乎 · 读书会,享会员专属权益",
        "sub_title": "精选好书免费读、折扣购,更有大咖领读经典免费听",
        "value": "0",
        "entrance_url": "zhihu://knowledge_market/purchase/1",
        "left_top_night_icon": "vip_free_night_icon",
        "book_vip_url": "zhihu://knowledge_market/purchase/1",
        "privilege": "会员立享折扣优惠",
        "cashier_url": "zhihu://wallet/cashier/933667430576807936",
        "subscription_id": 2,
        "guidance": "知乎·读书会会员免费读此书,还有更多知乎精选电子书等你免费读~",
        "right_type": 0
    },
    "svip_privileges": false,
    "generation": 1,
    "review_stats": {
        "bad": 3,
        "medium": 13,
        "review_count": 504,
        "score": "4.3",
        "good": 84
    },
    "is_subscribed": false,
    "preface": "一个陌生女人的电话,一起失踪事件,动物医生手岛伯朗卷入一场正在进行的犯罪事件。没有任何线索,甚至连同伴也不能完全信任。那个隐藏在家人中的凶手到底是谁?\n\n「维纳斯」不是某个人,而是存在于我们每个人心中令人疯狂的东西。恶不是一开始就存在,东野圭吾在这本书里诠释了好人是如何变成恶魔的。复杂的情节,反转再反转,但反转的是故事,还是人心?",
    "can_subscribe": false,
    "author_preface": "",
    "sku_id": "1092469025979895808",
    "global_anonymous": 0,
    "words_count": "17.3 万字",
    "table": "- 封面\n- 版权\n- 1\n- 2\n- 3\n- 4\n- 5\n- 6\n- 7\n- 8\n- 9\n- 10\n- 11\n- 12\n- 13\n- 14\n- 15\n- 16\n- 17\n- 18\n- 19\n- 20\n- 21\n- 22\n- 23\n- 24\n- 25\n- 26\n- 27\n- 28\n- 29\n- 30\n- 31",
    "book_version": "190804001",
    "publisher_name": "磨铁图书",
    "id": 119601575,
    "book_hash": "d41d8cd98f00b204e9800998ecf8427e",
    "review_count": 145,
    "title": "危险的维纳斯",
    "is_voted": false,
    "book_size": 911633,
    "comment_count": 504,
    "vote_count": 61,
    "score": 8.4,
    "anonymous_status": 0,
    "type": "ebook",
    "member_role": "normal",
    "on_shelf": false,
    "description": "「维纳斯」不是某个人,而是存在于我们每个人心中令人疯狂的东西。 ",
    "is_purchased": false,
    "qrcode_url": "",
    "is_access_reader": true,
    "collection": {
        "token": "book",
        "surname": "精选",
        "name": "知乎精选好书",
        "id": 0
    },
    "pintag": [],
    "authors": [
        {
            "avatar_url": "https://pic1.zhimg.com/50/v2-beeba36f24b04afe3e3fd689f8bc5c78_qhd.jpg?source=f11ebe26",
            "name": "东野圭吾",
            "url": "",
            "gender": 1,
            "type": "outer_author",
            "id": ""
        }
    ],
    "sales_bubble": {
        "is_pop": false
    },
    "privilege_status": 0,
    "coupons": {
        "status": 0
    },
    "is_own": false,
    "url": "https://www.zhihu.com/pub/book/119601575",
    "message": "",
    "cover": "https://pic1.zhimg.com/v2-c70fa52c486461e7d62f8c1f30269932_200x0.jpg?source=f11ebe26",
    "prompts": [],
    "promotion": {
        "pay_type": "wallet",
        "is_promotion": false,
        "zhihu_bean": 4500,
        "price": 4500,
        "origin_price": 4500
    },
    "on_shelves": true
}

可以通过include参数来包含额外信息,格式为:

encodeURIComponent('pub_time,dci_number,dci_cert,categories,authors[*].url_token,is_following,badge[?topics]')

目录接口

接口地址:

GET https://www.zhihu.com/api/v3/books/[bookId]/chapters
查看响应示例
{
  "deleted": [],
  "updated": [
    {
      "is_own": true,
      "hash": "",
      "title": "\u5c01\u9762",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 1,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "\u5c01\u9762",
      "chapter_uid": "1141324025534943232",
      "is_cover": true,
      "is_trial": true,
      "chapter_index": 0,
      "size": 115
    },
    {
      "is_own": true,
      "hash": "",
      "title": "\u7248\u6743",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 90,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "\u7248\u6743",
      "chapter_uid": "1141324025581142016",
      "is_cover": false,
      "is_trial": true,
      "chapter_index": 1,
      "size": 338
    },
    {
      "is_own": true,
      "hash": "",
      "title": "1",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 1905,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "1",
      "chapter_uid": "1092470252998324224",
      "is_cover": false,
      "is_trial": true,
      "chapter_index": 2,
      "size": 7085
    },
    {
      "is_own": true,
      "hash": "",
      "title": "2",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 9199,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "2",
      "chapter_uid": "1092470253497470976",
      "is_cover": false,
      "is_trial": true,
      "chapter_index": 3,
      "size": 32457
    },
    {
      "is_own": true,
      "hash": "",
      "title": "3",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5527,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "3",
      "chapter_uid": "1092470255116500992",
      "is_cover": false,
      "is_trial": true,
      "chapter_index": 4,
      "size": 19943
    },
    {
      "is_own": true,
      "hash": "",
      "title": "4",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 4666,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "4",
      "chapter_uid": "1092470256202776576",
      "is_cover": false,
      "is_trial": true,
      "chapter_index": 5,
      "size": 16702
    },
    {
      "is_own": true,
      "hash": "",
      "title": "5",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5192,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "5",
      "chapter_uid": "1092470257092042752",
      "is_cover": false,
      "is_trial": true,
      "chapter_index": 6,
      "size": 18162
    },
    {
      "is_own": false,
      "hash": "",
      "title": "6",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5271,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "6",
      "chapter_uid": "1092470258077626368",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 7,
      "size": 19415
    },
    {
      "is_own": false,
      "hash": "",
      "title": "7",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 4988,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "7",
      "chapter_uid": "1092470259155607552",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 8,
      "size": 18159
    },
    {
      "is_own": false,
      "hash": "",
      "title": "8",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 8740,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "8",
      "chapter_uid": "1092470260191559680",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 9,
      "size": 31912
    },
    {
      "is_own": false,
      "hash": "",
      "title": "9",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5849,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "9",
      "chapter_uid": "1092470261810544640",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 10,
      "size": 20711
    },
    {
      "is_own": false,
      "hash": "",
      "title": "10",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 9111,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "10",
      "chapter_uid": "1092470262951383040",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 11,
      "size": 33040
    },
    {
      "is_own": false,
      "hash": "",
      "title": "11",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 4047,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "11",
      "chapter_uid": "1092470264641785856",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 12,
      "size": 15062
    },
    {
      "is_own": false,
      "hash": "",
      "title": "12",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 4367,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "12",
      "chapter_uid": "1092470265463812096",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 13,
      "size": 15373
    },
    {
      "is_own": false,
      "hash": "",
      "title": "13",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5631,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "13",
      "chapter_uid": "1092470266302758912",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 14,
      "size": 20679
    },
    {
      "is_own": false,
      "hash": "",
      "title": "14",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 6821,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "14",
      "chapter_uid": "1092470267426791424",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 15,
      "size": 24744
    },
    {
      "is_own": false,
      "hash": "",
      "title": "15",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 6634,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "15",
      "chapter_uid": "1092470268815089664",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 16,
      "size": 24678
    },
    {
      "is_own": false,
      "hash": "",
      "title": "16",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 4763,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "16",
      "chapter_uid": "1092470270157205504",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 17,
      "size": 17556
    },
    {
      "is_own": false,
      "hash": "",
      "title": "17",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 6931,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "17",
      "chapter_uid": "1092470271147151360",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 18,
      "size": 25016
    },
    {
      "is_own": false,
      "hash": "",
      "title": "18",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5454,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "18",
      "chapter_uid": "1092470272539623424",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 19,
      "size": 20475
    },
    {
      "is_own": false,
      "hash": "",
      "title": "19",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 3543,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "19",
      "chapter_uid": "1092470273605009408",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 20,
      "size": 12967
    },
    {
      "is_own": false,
      "hash": "",
      "title": "20",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5824,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "20",
      "chapter_uid": "1092470274347360256",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 21,
      "size": 20885
    },
    {
      "is_own": false,
      "hash": "",
      "title": "21",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5694,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "21",
      "chapter_uid": "1092470275509133312",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 22,
      "size": 21252
    },
    {
      "is_own": false,
      "hash": "",
      "title": "22",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 6902,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "22",
      "chapter_uid": "1092470277400817664",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 23,
      "size": 25499
    },
    {
      "is_own": false,
      "hash": "",
      "title": "23",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 6125,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "23",
      "chapter_uid": "1092470278780776448",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 24,
      "size": 22558
    },
    {
      "is_own": false,
      "hash": "",
      "title": "24",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 8329,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "24",
      "chapter_uid": "1092470280013897728",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 25,
      "size": 29258
    },
    {
      "is_own": false,
      "hash": "",
      "title": "25",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5087,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "25",
      "chapter_uid": "1092470281523826688",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 26,
      "size": 18790
    },
    {
      "is_own": false,
      "hash": "",
      "title": "26",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 3892,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "26",
      "chapter_uid": "1092470282551427072",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 27,
      "size": 14197
    },
    {
      "is_own": false,
      "hash": "",
      "title": "27",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 2602,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "27",
      "chapter_uid": "1092470283327426560",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 28,
      "size": 9602
    },
    {
      "is_own": false,
      "hash": "",
      "title": "28",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 9352,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "28",
      "chapter_uid": "1092470283914514432",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 29,
      "size": 31836
    },
    {
      "is_own": false,
      "hash": "",
      "title": "29",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 5655,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "29",
      "chapter_uid": "1092470285504180224",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 30,
      "size": 19990
    },
    {
      "is_own": false,
      "hash": "",
      "title": "30",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 3254,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "30",
      "chapter_uid": "1092470286561177600",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 31,
      "size": 11921
    },
    {
      "is_own": false,
      "hash": "",
      "title": "31",
      "new_hash": "",
      "level": 1,
      "is_own_book": false,
      "word_count": 1501,
      "version": "190804001",
      "content_file": "",
      "chapter_title": "31",
      "chapter_uid": "1092470287207133184",
      "is_cover": false,
      "is_trial": false,
      "chapter_index": 32,
      "size": 5872
    }
  ],
  "version": "190804001"
}

获取章节的公钥接口

接口地址:

GET https://www.zhihu.com/api/v3/books/[bookId]/chapters/[chapterUid]/download_info
查看响应示例
{
  "key_hash": "E48FBEA6AACC8177E10AF1190421E92B",
  "key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgkB5vTONXv15SukpyFKKbkO3m\nMbZ8z4u8HwtV14qEoOJaOhh6pu75o6bojX3RFnWm3wHxFjmdJu1+JurChFiY2fxD\nQ+SZWXKzNvfK/fvi3JNMfgVfp0HcuCzKDWE+vPeactLeTNnjFRYlnaUygiwm0KNE\nhDDHw2/41xjcPLmPpQIDAQAB\n-----END PUBLIC KEY-----"
}

其中,key_hash参与签名的计算,key则用于加密客户端生成的一次性密码参数,加密后通过trans_key传给后端。

获取章节资源接口

接口地址:

POST https://www.zhihu.com/api/v3/books/[bookId]/chapters/[chapterUid]/download

client_id: 5774b305d2ae4469a2c9258956ea48
key_hash: E48FBEA6AACC8177E10AF1190421E92B
signature: 4b18cc7ffa392f6687901658c3f14b572e97c4d3
timestamp: 1723791957338
trans_key: 2eg1rIgURbckAtGLuWMEs4ZEWQzYugrFLWnZo+O+qu0GJ2xlACgZ73gIqc0b1M+Dh43RQzH2ElfeBq5mPYoCYWt1Wz56vlfRwcGSiZjImaweKfc/M1SHVCSRrbfKm8VZAhoKBvZKzUb2NV7fWQ9qetOAEIC5YcWEJwnaDlHDtW0=
查看响应示例
{
  "format_html_path": "http://book.zhimg.com/chapters/119601575/format_x86bskwm_190804001.html",
  "zip_path": "",
  "key_hash": "4DE1C8950141B0C56FA41E078F4AFFE9",
  "is_access_reader": true,
  "html_path": "http://book.zhimg.com/chapters/119601575/craw5akb_190804001.html",
  "download_url": "",
  "key": "kaxlNCdIny/oMkj3Vgi3YmIwqOoe1BtnnJu+pc/7pfE=",
  "message": "",
  "default_css": "https://book.zhimg.com/books/61E37BD8AC9209F6E3B5C18A7BF231E0.css",
  "css_path": [
    "https://book.zhimg.com/books/61E37BD8AC9209F6E3B5C18A7BF231E0.css",
    "https://book.zhimg.com/books/EC6E21149A53D434CE13B1F13F7C3545.css",
    "https://book.zhimg.com/books/1F451EC236F03D3A1166E4D8540AA024.css"
  ]
}

其中,html_path字段即为该章节的文件地址,直接下载该文件可得到密文内容。key为解密该内容的密码,只不过这个密码不是明文,是经过我们前面生成的那个一次性密码用aes-128-cfb8算法加密得到的。

加解密算法分析

流程整个梳理

  1. 调用【获取章节的公钥接口】获取到对应章节的公钥keykeyHash参数
  2. 调用【获取章节资源接口】获取章节的html路径,以及解密所需的key,参数的签名及加密算法见下面的代码
// 计算 transKey,使用公钥 key 对 secret 进行加密,即得到 trans_key
function getTransKey(key: string, secret: string) {
    const body = Buffer.alloc(128 - secret.length)
    const buf = Buffer.concat([body, Buffer.from(secret)])
    return crypto.publicEncrypt({
        key: key,
        padding: 3,
    }, buf).toString('base64')
}

// 计算签名
function signPayload(payload: string[]) {
    const e = crypto.createHmac("sha1", "key")
    for (const value of payload) {
        e.update(value)
    }
    return e.digest("hex")
}

// 这两个参数由【获取章节的公钥接口】获取
const key = ''
const key_hash = ''

const client_id = "5774b305d2ae4469a2c9258956ea48";
const timestamp = Number(new Date())

// 生成临时密码
const secret = Array.from({length: 16}).map(() => Math.floor(16 * Math.random()).toString(16).toUpperCase()).join("")

// 对临时密码进行公钥加密,以便在网络上传输
const transKey = getTransKey(key, secret)
const signature = signPayload([chapterUid, transKey, client_id, timestamp, key_hash])
  1. 下载 html 资源,拿到密文
  2. 还原密钥并进行解密
// 用 aes-128-cfb8 算法解密数据
function decrypt(secret, data, encoding) {
    let iv, body;
    if ("string" == typeof data) {
        let buf = Buffer.from(data, "base64");
        iv = Buffer.alloc(16)
        body = Buffer.alloc(buf.length - 16)
        buf.copy(iv, 0, 0, 16)
        buf.copy(body, 0, 16)
    } else {
        iv = data.slice(0, 16)
        body = data.slice(16)
    }
    return crypto.createDecipheriv("aes-128-cfb8", secret, iv).update(body, null, encoding)
}

// secret 即上面生成的临时密码,key是【获取章节资源接口】返回的
const htmlSecret = decrypt(secret, key, 'utf8')

// buffer 为密文内容
const plainText = decrypt(htmlSecret, buffer, 'utf8')

总结

整个加解密的过程如下:

知乎服务器对每本书的每个章节的html内容用aes-128-cfb8对称加密算法进行加密,并保存该章节对应的密码(每个章节都有自己的密码,我们称这个密码为k1)。

然后浏览器在查看该章节的内容时,会在前端生成一个临时密码(我们称为k2),这个k2并不会在网络上传输,只会保留在本地内存中。

然后前端用一对公私钥中的公钥对k2进行加密并传给知乎服务器,由于k2是用非对称加密算法加密的,所以只有知乎才能解密出k2的内容。

然后知乎服务器用k2作为密码,使用对称加密算法aes-128-cfb8k1进行加密,并返回给前端(【获取章节资源接口】所返回的key)。

然后前端使用相同的对称加密算法aes-128-cfb8解密出k1(因为加密的key就是k2),然后下载html密文,并用k1解密其内容。

最后,至于是每个章节都有一对公私钥,还是说所有书籍共用一对公私钥,都无所谓。因为通过网络的数据都不可能解密出k2的内容。